Pii
PII-шифрование: AES-256-GCM blob, хеш телефона, маскировка для UI.
Архитектура¶
flowchart TD
Input["PiiInput: phone, firstName, lastName,\nnationalIdNumber, dateOfBirth,\nnationality, countryOfResidence,\naddress, dateOfIssue, dateOfExpiry,\ngender, employer"] --> Writer[PiiProfileWriter]
Writer --> Crypto["PiiCryptoService\nAES-256-GCM → encryptedPii blob"]
Writer --> Hash["PiiHashService\nHMAC-SHA256 + pepper → phone / nationalId hash"]
Writer --> Mask["PiiMaskService\n+66 8XX-XXXX → phoneMasked\nJ*** → firstName/lastName masked"]
Writer --> DB[(user_profile)]
Ключ шифрования читается из Serverpod passwords: piiEncryptionKey (base64, 32 байта).
Peppers для хешей: piiPhoneHashPepper, piiNatIdHashPepper (base64).
Что где хранится¶
Поле в user_profile |
Хранение | Назначение |
|---|---|---|
phone |
HMAC-SHA256 + pepper hash | Поиск по номеру без расшифровки |
phoneMasked |
Маска (+66 8XX-XXXX) |
Отображение в UI |
nationalIdNumber |
HMAC-SHA256 + pepper hash | Поиск по ID без расшифровки |
nationalIdMasked |
Маска | Отображение в UI |
firstName |
Маска (J***) |
Отображение в UI |
lastName |
Маска | Отображение в UI |
fullName |
Маска | Отображение в UI |
encryptedPii |
AES-256-GCM blob | Все PII-поля: phone, firstName, lastName, fullName, nationalIdNumber, dateOfBirth, nationality, countryOfResidence, address, dateOfIssue, dateOfExpiry, gender, employer |
languageCode |
Открыто | Не PII |
accountTier |
Открыто | Не PII |
isLocked |
Открыто | Не PII |
avatarUrl |
Открыто | URL в S3 — не содержит PII |
encryptPii |
Открыто | Флаг: false для admin-created аккаунтов без шифрования |
PII blob layout¶
- Алгоритм:
AesGcm.with256bits()(пакетcryptography) - Nonce: генерируется случайно при каждом шифровании
- Содержимое ciphertext: JSON-объект со всеми PII-полями из
PiiInput.toJson() - Версия
0x01— единственная поддерживаемая; неизвестная версия →UnsupportedError
PiiProfileWriter¶
ЕДИНСТВЕННЫЙ путь записи PII в user_profile. Прямые вызовы UserProfile.db.insertRow / updateRow для PII-полей запрещены.
| Свойство | Описание |
|---|---|
| Идемпотентность | upsert по userId: insert если профиля нет, update если есть |
| Транзакции | принимает опциональный Transaction? параметр; может работать внутри транзакции |
Флаг encryptPii=false |
при false — маскировка не применяется, данные сохраняются как есть (для admin-created аккаунтов) |
PiiDecryptionService¶
| Метод | Для кого | Audit log |
|---|---|---|
decryptForOwner(session, userId) |
Сам пользователь (owner) | Нет — audit на слое endpoint |
decryptForAdmin(session, userId) |
Оператор Admin Panel | Нет — audit на слое endpoint; endpoint обязан записать action=pii_revealed с reason |
decryptOcrForAdmin(session, kycId) |
Оператор (OCR-данные KYC) | Нет — audit на слое endpoint; endpoint обязан записать action=ocr_revealed |
PiiDecryptionService — чистый сервис без побочных эффектов. Авторизация, проверка ролей и аудит — ответственность вызывающего endpoint.
decryptOcrForAdmin поддерживает два layout OCR-JSON: обёрнутый ({ocrData: {...}}) и legacy flat ({fullName: ..., ...}).
Дополнительно _buildDecrypted вычисляет nfcAvailable = NfcConfig.globalEnabled && profile.nfcEnabled — в БД не хранится, вычисляется на каждый запрос.
PDPA compliance¶
| Статус | Механизм |
|---|---|
| ✅ Шифрование at rest | AES-256-GCM blob в encryptedPii |
| ✅ Hard delete | HardDeleteArchivedUsersFutureCall удаляет через 30 дней после архивации |
| ✅ Хеш вместо plaintext | phone и nationalIdNumber хранятся только как HMAC-SHA256 |
| [PLAN] S3 файлы | Фото документов KYC в S3 не удаляются при hard-delete — PDPA gap (Phase 3) |
| [PLAN] Право на экспорт | Export PII для владельца не реализован — PDPA gap |