Перейти к содержанию

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

[1 byte version=0x01][12 bytes nonce][ciphertext][16 bytes GCM tag]
  • Алгоритм: 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