Created
February 13, 2026 12:09
-
-
Save tripolskypetr/1497e3a2d724a568f415161dc8e5d167 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # GSI Mobile SDK | |
| ## Техническая документация | |
| **Версия документа:** 1.0 | |
| **Дата:** Декабрь 2024 | |
| --- | |
| ## Глоссарий | |
| | Термин | Расшифровка | | |
| |--------|-------------| | |
| | ГЦИ | Главный центр информатизации — контрагент, обслуживающий запросы мобильных SDK через прокси банков | | |
| | ГЦП | Государственный центр персонализации — государственный орган, хранящий персональные данные граждан (аналог МВД) | | |
| | ПИНФЛ | Персональный идентификационный номер физического лица | | |
| | mTLS | Mutual TLS — двусторонняя аутентификация с использованием клиентских сертификатов | | |
| | IPSec | Internet Protocol Security — протокол защиты сетевого трафика на уровне IP | | |
| | Liveness | Проверка «живости» — подтверждение, что перед камерой находится живой человек, а не фотография или видеозапись | | |
| | JWT | JSON Web Token — стандарт токенов для безопасной передачи данных | | |
| --- | |
| ## 1. Общая информация о продукте | |
| GSI Mobile SDK — программный комплекс для биометрической верификации личности граждан Республики Узбекистан. SDK обеспечивает проверку соответствия лица пользователя данным в государственных реестрах с использованием технологии распознавания лиц и пассивного liveness detection. | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Официальное название | gsi-mobile-sdk | | |
| | Платформы | iOS, Android, Flutter | | |
| | Минимальная версия iOS | 13.0 | | |
| | Минимальная версия Android | 6.0 (API level 23) | | |
| | Языки SDK | Kotlin (Android), Swift (iOS) | | |
| SDK предоставляет доступ к следующим данным при успешной верификации: ПИНФЛ, серия и номер паспорта, ФИО, дата рождения, история предыдущих паспортов. | |
| --- | |
| ## 2. Архитектура решения | |
| ### 2.1 Схема взаимодействия компонентов | |
| Архитектура системы построена на принципе сегментации сети с разделением на публичную зону (Public FQDN) и закрытую сеть. | |
| ```mermaid | |
| flowchart LR | |
| subgraph public["Public FQDN"] | |
| SDK["Мобильное SDK"] | |
| end | |
| subgraph bank["Инфраструктура банка"] | |
| PROXY["mTLS Proxy"] | |
| BACKEND["Backend банка"] | |
| end | |
| subgraph closed["Закрытая сеть"] | |
| GCI["ГЦИ"] | |
| GCP["ГЦП"] | |
| end | |
| SDK -->|"Mutual SSL"| PROXY | |
| PROXY -->|"IPSec VPN"| GCI | |
| GCI -->|"Внутренний канал"| GCP | |
| style public fill:#e8f5e9 | |
| style closed fill:#fff3e0 | |
| style bank fill:#e3f2fd | |
| ``` | |
| Мобильное SDK находится в публичной зоне и взаимодействует с прокси-сервером банка через Mutual SSL. Прокси-сервер банка передаёт запросы в закрытую сеть ГЦИ через IPSec VPN туннель. ГЦИ обращается к ГЦП для получения эталонных биометрических данных. | |
| ### 2.2 Serverless-режим с криптографической защитой | |
| В данном режиме SDK получает подписанный ответ напрямую. Защита данных обеспечивается JWT-подписью ответа сервера ГЦИ. | |
| ```mermaid | |
| sequenceDiagram | |
| participant SDK as Мобильное SDK | |
| participant Bank as mTLS Proxy банка | |
| participant GCI as ГЦИ | |
| participant GCP as ГЦП | |
| SDK->>Bank: Видео + accessToken (JWT) | |
| Bank->>GCI: Проксирование запроса (IPSec) | |
| GCI->>GCP: Запрос эталонного фото | |
| GCP-->>GCI: Эталонное фото | |
| GCI->>GCI: Liveness + сравнение лиц | |
| GCI-->>Bank: Ответ с JWT подписью (HS256) | |
| Bank-->>SDK: Персональные данные + signature | |
| SDK->>SDK: Отображение результата | |
| ``` | |
| Банк получает полные персональные данные в ответе. JWT-подпись позволяет верифицировать подлинность ответа на backend банка. | |
| ### 2.3 Webhook-режим с пустым body | |
| В данном режиме SDK получает пустые строки вместо персональных данных. Реальные данные отправляются на webhook банка напрямую с прокси-сервера. | |
| ```mermaid | |
| sequenceDiagram | |
| participant SDK as Мобильное SDK | |
| participant Proxy as mTLS Proxy банка | |
| participant Backend as Backend банка | |
| participant GCI as ГЦИ | |
| participant GCP as ГЦП | |
| SDK->>Proxy: Видео + accessToken (JWT с verificationId) | |
| Proxy->>GCI: Проксирование запроса (IPSec) | |
| GCI->>GCP: Запрос эталонного фото | |
| GCP-->>GCI: Эталонное фото | |
| GCI->>GCI: Liveness + сравнение лиц | |
| GCI-->>Proxy: Персональные данные + signature | |
| Proxy->>Proxy: Извлечение verificationId из JWT | |
| Proxy->>Backend: Webhook с данными и verificationId | |
| Proxy->>Proxy: Замена данных на пустые строки | |
| Proxy-->>SDK: Пустые строки + статус OK | |
| SDK->>SDK: Отображение успеха | |
| Backend->>Backend: Сохранение данных по verificationId | |
| ``` | |
| Преимущества webhook-режима: персональные данные не передаются на клиентское устройство, что исключает их перехват на стороне пользователя. | |
| --- | |
| ## 3. Состав SDK и комплект поставки | |
| ### 3.1 Комплект поставки | |
| Поставка осуществляется в виде ZIP-архива, содержащего следующие компоненты: | |
| - `.framework` для iOS (TGFISBIN.xcframework) | |
| - `.aar` для Android (facedetectsdk.aar) | |
| - Flutter bindings для кроссплатформенной разработки | |
| - OpenAPI-схема для интеграции через REST API | |
| - Пример кода C# прокси для настройки Mutual SSL | |
| - Демо-приложения для iOS, Android, Flutter | |
| - Документация и changelog | |
| Для получения обновлений предусмотрено подключение Maven-репозитория. Flutter-версия опубликована в pub.dev. | |
| ### 3.2 Внешние зависимости | |
| **Android:** | |
| ```gradle | |
| implementation("androidx.camera:camera-core:1.4.2") | |
| implementation("androidx.camera:camera-lifecycle:1.4.2") | |
| implementation("androidx.camera:camera-video:1.4.2") | |
| implementation("androidx.camera:camera-view:1.4.2") | |
| implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") | |
| implementation("com.google.mlkit:face-detection:16.1.7") | |
| implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") | |
| implementation("com.scottyab:rootbeer-lib:0.1.1") | |
| implementation("com.googlecode.mp4parser:isoparser:1.1.22") | |
| implementation("androidx.camera:camera-camera2:1.3.0") | |
| ``` | |
| **iOS:** Используются стандартные библиотеки платформы, дополнительные зависимости не требуются. | |
| ### 3.3 Инфраструктурные требования | |
| | Окружение | Требования | | |
| |-----------|------------| | |
| | Тестирование | Домен `face.mbabm.uz` (Public FQDN) | | |
| | Production | IPSec-туннель от сервера ГЦИ до банка | | |
| | Прокси | Mutual SSL proxy на стороне банка (nginx или C#) | | |
| --- | |
| ## 4. Функциональность Face IDS | |
| ### 4.1 Типы операций | |
| SDK поддерживает верификацию 1:1 с поиском по серии/номеру паспорта или ПИНФЛ. Применимо для регистрации пользователей и валидации платежей. | |
| ### 4.2 Liveness Detection | |
| Реализован пассивный liveness по видео — специальные жесты от пользователя не требуются. Система проверяет: открыты ли глаза, одно ли лицо в кадре, положение головы. | |
| ### 4.3 Требования к съёмке | |
| | Параметр | Требование | | |
| |----------|------------| | |
| | Освещение | По ГОСТ для жилых помещений (не более 300 люкс) | | |
| | Угол наклона головы | Не более 30° | | |
| | Количество лиц | Строго одно лицо в кадре | | |
| | Расстояние | Оптимально 30-50 см от камеры | | |
| ### 4.4 Входные данные | |
| SDK принимает только видео. Изображения не поддерживаются. Из видео автоматически извлекается лучший кадр, центрированный по лицу. | |
| ### 4.5 Результат верификации | |
| При успешной верификации возвращается объект со следующими полями: | |
| | Поле | Тип | Описание | | |
| |------|-----|----------| | |
| | `pin` | String | ПИНФЛ | | |
| | `firstName` | String | Имя | | |
| | `lastName` | String | Фамилия | | |
| | `patronym` | String | Отчество | | |
| | `birthDate` | String | Дата рождения (DD.MM.YYYY) | | |
| | `docSeria` | String | Серия паспорта | | |
| | `docNumber` | String | Номер паспорта | | |
| | `score` | Double | Confidence score (0.0 — 1.0) | | |
| | `photo` | String | Фото лица из видео (base64) | | |
| | `signature` | String | JWT подпись ответа | | |
| При неуспешной верификации возвращается `null` без дополнительных данных. | |
| ### 4.6 Коды ошибок | |
| При неуспешной верификации SDK возвращает код ошибки в поле `errorCode`. Коды сгруппированы по категориям для удобства обработки в кастомной вёрстке. | |
| #### 4.6.1 Ошибки ГЦП (GSI API Errors) | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `GSI_API_ERROR` | ГЦП недоступен | Сервер ГЦП не отвечает или вернул ошибку | Попробуйте позже | | |
| | `GSI_NO_DATA` | ГЦП не передал данные | ГЦП не нашёл данные по указанным реквизитам | Проверьте серию и номер паспорта | | |
| #### 4.6.2 Ошибки Liveness-проверки | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `LIVENESS_NOT_ALIVE` | Не пройден liveness (#1) | Система определила, что перед камерой не живой человек | Повторите попытку, держите телефон ровно | | |
| | `LIVENESS_NO_AVERAGE` | Не пройден liveness (#2) | Не удалось вычислить усреднённый кадр | Повторите попытку с лучшим освещением | | |
| | `LIVENESS_NO_BEST_FACE` | Не пройден liveness (#3) | Не найден качественный кадр с лицом | Повторите попытку, смотрите в камеру | | |
| | `LIVENESS_NO_NORMALIZED` | Не пройден liveness (#4) | Не удалось нормализовать изображение лица | Повторите попытку | | |
| | `LIVENESS_SCORE_TOO_LOW` | Не пройден liveness (#5) | Низкий показатель liveness | Повторите попытку, не используйте фото | | |
| #### 4.6.3 Ошибки детекции лица в видео | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `LIVENESS_MULTIPLE_FACES` | Несколько лиц в кадре | В видео обнаружено более одного лица | Убедитесь, что в кадре только вы | | |
| #### 4.6.4 Ошибки положения глаз и головы | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `EYES_CLOSED_OR_HIDDEN` | Глаза закрыты или не видны | Система не может определить открытые глаза | Откройте глаза, снимите очки | | |
| | `HEAD_TURNED` | Голова повернута | Угол наклона головы превышает допустимый | Держите голову прямо, смотрите в камеру | | |
| #### 4.6.5 Ошибки эталонного фото из ГЦП | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `GSI_PHOTO_MULTIPLE_FACES` | Фото из ГЦП содержит несколько лиц | Эталонное фото некорректно | Обратитесь в поддержку | | |
| | `GSI_PHOTO_NO_FACE` | Фото из ГЦП не содержит лицо | На эталонном фото не найдено лицо | Обратитесь в поддержку | | |
| #### 4.6.6 Ошибки сравнения лиц | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `FACEIDS_API_ERROR` | Ошибка API FaceIds | Внутренняя ошибка сервиса сравнения | Повторите попытку позже | | |
| | `SIMILARITY_SCORE_TOO_LOW` | Низкое сходство с фото из ГЦП | Лицо не совпадает с эталоном | Убедитесь, что это ваш паспорт | | |
| #### 4.6.7 Ошибки валидации документа | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `PASSPORT_INVALID` | Паспорт недействителен | Документ аннулирован или не существует | Проверьте данные паспорта | | |
| | `DOCUMENT_EXPIRED` | Истёк срок действия документа | Паспорт просрочен | Обновите документ | | |
| #### 4.6.8 Ошибки валидации запроса | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `INVALID_PAYLOAD` | Неверные параметры запроса | Отсутствуют обязательные поля или неверный формат | Проверьте данные | | |
| #### 4.6.9 Общие ошибки | |
| | Код | Сообщение | Описание | Рекомендация пользователю | | |
| |-----|-----------|----------|---------------------------| | |
| | `UNKNOWN_ERROR` | Неизвестная ошибка | Непредвиденная ошибка сервера | Повторите попытку позже | | |
| #### 4.6.10 Пример обработки ошибок в кастомной вёрстке | |
| ```typescript | |
| const VerificationErrorMessage: Record<string, string> = { | |
| // Ошибки ГЦП | |
| "GSI_API_ERROR": "ГЦП недоступен. Попробуйте позже", | |
| "GSI_NO_DATA": "ГЦП не передал данные. Проверьте серию и номер паспорта", | |
| // Ошибки Liveness | |
| "LIVENESS_NOT_ALIVE": "Не пройден liveness (#1)", | |
| "LIVENESS_NO_AVERAGE": "Не пройден liveness (#2)", | |
| "LIVENESS_NO_BEST_FACE": "Не пройден liveness (#3)", | |
| "LIVENESS_NO_NORMALIZED": "Не пройден liveness (#4)", | |
| "LIVENESS_SCORE_TOO_LOW": "Не пройден liveness (#5)", | |
| // Ошибки детекции лица | |
| "LIVENESS_MULTIPLE_FACES": "Фото содержит больше одного лица. Обратитесь в поддержку", | |
| // Ошибки положения | |
| "EYES_CLOSED_OR_HIDDEN": "Глаза закрыты или не видны", | |
| "HEAD_TURNED": "Голова повернута", | |
| // Ошибки эталонного фото | |
| "GSI_PHOTO_MULTIPLE_FACES": "Фото из ГЦП содержит больше одного лица. Обратитесь в поддержку", | |
| "GSI_PHOTO_NO_FACE": "Фото из ГЦП не содержит лицо. Обратитесь в поддержку", | |
| // Ошибки сравнения | |
| "FACEIDS_API_ERROR": "Ошибка из API FaceIds", | |
| "SIMILARITY_SCORE_TOO_LOW": "Низкое сходство с фото из ГЦП", | |
| // Ошибки документа | |
| "PASSPORT_INVALID": "Паспорт недействителен", | |
| "DOCUMENT_EXPIRED": "Истек срок действия документа", | |
| // Ошибки запроса | |
| "INVALID_PAYLOAD": "Неверные параметры запроса. Проверьте данные", | |
| // Общие | |
| "UNKNOWN_ERROR": "Произошла неизвестная ошибка", | |
| }; | |
| function getErrorMessage(errorCode: string): string { | |
| return VerificationErrorMessage[errorCode] || "Произошла ошибка"; | |
| } | |
| function isRetryableError(errorCode: string): boolean { | |
| const retryable = [ | |
| "GSI_API_ERROR", | |
| "FACEIDS_API_ERROR", | |
| "UNKNOWN_ERROR", | |
| "LIVENESS_NOT_ALIVE", | |
| "LIVENESS_NO_AVERAGE", | |
| "LIVENESS_NO_BEST_FACE", | |
| "LIVENESS_NO_NORMALIZED", | |
| "LIVENESS_SCORE_TOO_LOW", | |
| "EYES_CLOSED_OR_HIDDEN", | |
| "HEAD_TURNED", | |
| "LIVENESS_MULTIPLE_FACES", | |
| ]; | |
| return retryable.includes(errorCode); | |
| } | |
| function requiresSupport(errorCode: string): boolean { | |
| const supportRequired = [ | |
| "GSI_PHOTO_MULTIPLE_FACES", | |
| "GSI_PHOTO_NO_FACE", | |
| ]; | |
| return supportRequired.includes(errorCode); | |
| } | |
| ``` | |
| --- | |
| ## 5. API и методы SDK | |
| ### 5.1 Паттерн инициализации | |
| SDK использует паттерн Builder для всех платформ. Единственный публичный метод — `startFaceDetection` / `startVerification`. Метод открывает полноэкранное activity с WebView и возвращает объект с персональными данными при успехе или `null` при неудаче. | |
| ### 5.2 Android (Kotlin) | |
| ```kotlin | |
| class MainActivity : ComponentActivity() { | |
| private lateinit var resultTextView: TextView | |
| private val launcher = registerForActivityResult( | |
| ActivityResultContracts.StartActivityForResult() | |
| ) { result -> | |
| if (result.resultCode == RESULT_OK) { | |
| result.data?.let { intent -> | |
| val person = FaceDetectionSdk.getResultFromIntent(intent) | |
| resultTextView.text = """ | |
| ПИНФЛ: ${person.pin} | |
| ФИО: ${person.lastName} ${person.firstName} ${person.patronym} | |
| Дата рождения: ${person.birthDate} | |
| Паспорт: ${person.docSeria}${person.docNumber} | |
| Score: ${person.score} | |
| """.trimIndent() | |
| } | |
| } else { | |
| resultTextView.text = "Верификация не пройдена" | |
| } | |
| } | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| setContentView(R.layout.activity_main) | |
| resultTextView = findViewById(R.id.resultTextView) | |
| findViewById<Button>(R.id.verifyButton).setOnClickListener { | |
| startVerification() | |
| } | |
| } | |
| private fun startVerification() { | |
| FaceDetectionSdk.Builder(this) | |
| .setAccessToken("YOUR_JWT_TOKEN") | |
| .setUrl("https://your-proxy.bank.uz/") | |
| .setLanguage("ru") | |
| .setClientP12Base64(getClientCertificate(), "cert_password") | |
| .setEnableMutualSSL(true) | |
| .build() | |
| .startFaceDetection(launcher) | |
| } | |
| private fun getClientCertificate(): String { | |
| // Получить сертификат с сервера банка динамически | |
| // КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в APK | |
| return "BASE64_P12_FROM_SERVER" | |
| } | |
| } | |
| ``` | |
| ### 5.3 iOS (Swift) | |
| ```swift | |
| import SwiftUI | |
| import TGFISBIN | |
| struct ContentView: View { | |
| @State private var resultText = "" | |
| @State private var personPhoto: UIImage? | |
| var body: some View { | |
| VStack(spacing: 20) { | |
| if let photo = personPhoto { | |
| Image(uiImage: photo) | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 150, height: 150) | |
| .clipShape(Circle()) | |
| } | |
| Text(resultText) | |
| .multilineTextAlignment(.center) | |
| Button("Начать верификацию") { | |
| startVerification() | |
| } | |
| } | |
| .padding() | |
| } | |
| private func startVerification() { | |
| let sslConfig = SSLConfig( | |
| clientCertificateBase64: getClientCertificate(), | |
| clientCertificatePassword: "cert_password", | |
| enableMutualSSL: true | |
| ) | |
| let config = SDKConfig( | |
| locale: .ru, | |
| token: "YOUR_JWT_TOKEN", | |
| baseUrl: "https://your-proxy.bank.uz/", | |
| ssl: sslConfig, | |
| uiConfig: nil, | |
| resultType: .standard, | |
| useNative: true, | |
| privacyPolicyUrl: nil | |
| ) | |
| GSI.shared.start(config: config) { result in | |
| guard let result = result, | |
| let person = result.standardResult else { | |
| resultText = "Верификация не пройдена" | |
| return | |
| } | |
| resultText = """ | |
| ПИНФЛ: \(person.pin) | |
| ФИО: \(person.lastName) \(person.firstName) \(person.patronym) | |
| Дата рождения: \(person.birthDate) | |
| Паспорт: \(person.docSeria)\(person.docNumber) | |
| Score: \(String(format: "%.0f%%", person.score * 100)) | |
| """ | |
| if let photoData = Data(base64Encoded: person.photo) { | |
| personPhoto = UIImage(data: photoData) | |
| } | |
| } | |
| } | |
| private func getClientCertificate() -> String { | |
| // Получить сертификат с сервера банка динамически | |
| // КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в IPA | |
| return "BASE64_P12_FROM_SERVER" | |
| } | |
| } | |
| ``` | |
| ### 5.4 Flutter (Dart) | |
| ```dart | |
| import 'package:flutter/material.dart'; | |
| import 'package:facedetect_tadi_sdk/facedetect_tadi_sdk.dart'; | |
| import 'dart:convert'; | |
| class VerificationScreen extends StatefulWidget { | |
| const VerificationScreen({super.key}); | |
| @override | |
| State<VerificationScreen> createState() => _VerificationScreenState(); | |
| } | |
| class _VerificationScreenState extends State<VerificationScreen> { | |
| final _sdk = FacedetectTadiSdk(); | |
| String _resultText = ''; | |
| MemoryImage? _personPhoto; | |
| Future<void> _startVerification() async { | |
| final config = SdkConfig( | |
| accessToken: 'YOUR_JWT_TOKEN', | |
| baseUrl: 'https://your-proxy.bank.uz/', | |
| language: 'ru', | |
| enableMutualSSL: true, | |
| clientCertificateBase64: await _getClientCertificate(), | |
| clientCertificatePassword: 'cert_password', | |
| ); | |
| final result = await _sdk.startFaceDetection(config); | |
| if (result == null) { | |
| setState(() { | |
| _resultText = 'Верификация не пройдена'; | |
| _personPhoto = null; | |
| }); | |
| return; | |
| } | |
| setState(() { | |
| _resultText = ''' | |
| ПИНФЛ: ${result.pin} | |
| ФИО: ${result.lastName} ${result.firstName} ${result.patronym} | |
| Дата рождения: ${result.birthDate} | |
| Паспорт: ${result.docSeria}${result.docNumber} | |
| Score: ${(result.score * 100).toStringAsFixed(0)}% | |
| '''; | |
| if (result.photo.isNotEmpty) { | |
| _personPhoto = MemoryImage(base64Decode(result.photo)); | |
| } | |
| }); | |
| } | |
| Future<String> _getClientCertificate() async { | |
| // Получить сертификат с сервера банка динамически | |
| // КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в приложение | |
| return 'BASE64_P12_FROM_SERVER'; | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar(title: const Text('Верификация')), | |
| body: Center( | |
| child: Padding( | |
| padding: const EdgeInsets.all(20), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| if (_personPhoto != null) | |
| CircleAvatar(radius: 75, backgroundImage: _personPhoto), | |
| const SizedBox(height: 20), | |
| Text(_resultText, textAlign: TextAlign.center), | |
| const SizedBox(height: 30), | |
| ElevatedButton( | |
| onPressed: _startVerification, | |
| child: const Text('Начать верификацию'), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| ``` | |
| ### 5.5 REST API Endpoints | |
| SDK использует следующие endpoints для взаимодействия с сервером: | |
| ``` | |
| POST /api/v1/gsi/verify — multipart/form-data (видео файлом) | |
| POST /api/v1/gsi/verify_b64 — JSON (видео в base64) | |
| ``` | |
| ### 5.6 OpenAPI-схема | |
| ```yaml | |
| openapi: 3.0.3 | |
| info: | |
| title: GSI Verification API | |
| description: API для верификации граждан через систему ГЦП с проверкой liveness | |
| version: 1.0.0 | |
| servers: | |
| - url: /api/v1/gsi | |
| description: Production server | |
| paths: | |
| /verify_b64: | |
| post: | |
| summary: Верификация с видео в формате base64 | |
| description: | | |
| Выполняет полную верификацию гражданина: | |
| - Проверка данных в ГЦП | |
| - Liveness-проверка видео | |
| - Сравнение лица с фото из ГЦП | |
| - Проверка срока действия документа | |
| operationId: verifyB64 | |
| tags: | |
| - Verification | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VerificationRequest' | |
| responses: | |
| '200': | |
| description: Верификация успешна | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VerificationSuccessResponse' | |
| '403': | |
| description: Верификация не пройдена | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VerificationErrorResponse' | |
| /verify: | |
| post: | |
| summary: Верификация с видео в формате multipart/form-data | |
| operationId: verify | |
| tags: | |
| - Verification | |
| requestBody: | |
| required: true | |
| content: | |
| multipart/form-data: | |
| schema: | |
| $ref: '#/components/schemas/VerificationRequestMultipart' | |
| responses: | |
| '200': | |
| description: Верификация успешна | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VerificationSuccessResponse' | |
| '403': | |
| description: Верификация не пройдена | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VerificationErrorResponse' | |
| components: | |
| schemas: | |
| VerificationRequest: | |
| type: object | |
| required: | |
| - serviceName | |
| - clientId | |
| - userId | |
| - token | |
| - requestId | |
| - birth_date | |
| - doc_number | |
| - doc_seria | |
| - doc_pinfl | |
| - video | |
| properties: | |
| serviceName: | |
| type: string | |
| description: Название сервиса | |
| clientId: | |
| type: string | |
| description: Идентификатор клиента | |
| userId: | |
| type: string | |
| description: Идентификатор пользователя | |
| token: | |
| type: string | |
| description: Токен авторизации | |
| requestId: | |
| type: string | |
| description: Уникальный идентификатор запроса | |
| birth_date: | |
| type: string | |
| format: date | |
| description: Дата рождения (DD.MM.YYYY) | |
| doc_number: | |
| type: string | |
| description: Номер документа | |
| doc_seria: | |
| type: string | |
| description: Серия документа | |
| doc_pinfl: | |
| type: string | |
| description: ПИНФЛ | |
| video: | |
| type: string | |
| format: byte | |
| description: Видео в формате base64 | |
| VerificationRequestMultipart: | |
| type: object | |
| required: | |
| - serviceName | |
| - clientId | |
| - userId | |
| - token | |
| - requestId | |
| - birth_date | |
| - doc_number | |
| - doc_seria | |
| - doc_pinfl | |
| - video | |
| properties: | |
| serviceName: | |
| type: string | |
| clientId: | |
| type: string | |
| userId: | |
| type: string | |
| token: | |
| type: string | |
| requestId: | |
| type: string | |
| birth_date: | |
| type: string | |
| format: date | |
| doc_number: | |
| type: string | |
| doc_seria: | |
| type: string | |
| doc_pinfl: | |
| type: string | |
| video: | |
| type: string | |
| format: binary | |
| description: Видео файл (MP4) | |
| VerificationSuccessResponse: | |
| type: object | |
| required: | |
| - status | |
| - clientId | |
| - requestId | |
| - userId | |
| - serviceName | |
| - data | |
| properties: | |
| status: | |
| type: string | |
| enum: [ok] | |
| clientId: | |
| type: string | |
| requestId: | |
| type: string | |
| userId: | |
| type: string | |
| serviceName: | |
| type: string | |
| data: | |
| $ref: '#/components/schemas/PersonData' | |
| PersonData: | |
| type: object | |
| properties: | |
| pin: | |
| type: string | |
| description: ПИНФЛ | |
| doc_pinfl: | |
| type: string | |
| doc_seria: | |
| type: string | |
| doc_number: | |
| type: string | |
| namelat: | |
| type: string | |
| description: Имя (латиница) | |
| surnamelat: | |
| type: string | |
| description: Фамилия (латиница) | |
| patronymlat: | |
| type: string | |
| description: Отчество (латиница) | |
| namecyr: | |
| type: string | |
| description: Имя (кириллица) | |
| surnamecyr: | |
| type: string | |
| description: Фамилия (кириллица) | |
| patronymcyr: | |
| type: string | |
| description: Отчество (кириллица) | |
| birth_date: | |
| type: string | |
| format: date | |
| birthplace: | |
| type: string | |
| citizenship: | |
| type: string | |
| citizenshipid: | |
| type: string | |
| gender: | |
| type: string | |
| description: "1 - мужской, 2 - женский" | |
| livestatus: | |
| type: string | |
| description: Статус жизни | |
| current_pinpp: | |
| type: string | |
| current_document: | |
| type: string | |
| signature: | |
| type: string | |
| description: JWT подпись | |
| capture: | |
| type: string | |
| format: byte | |
| description: Изображение лица (base64) | |
| photo: | |
| type: string | |
| format: byte | |
| description: Эталонное фото из ГЦП (base64) | |
| score: | |
| type: number | |
| format: float | |
| minimum: 0 | |
| maximum: 1 | |
| createdAt: | |
| type: string | |
| format: date-time | |
| VerificationErrorResponse: | |
| type: object | |
| required: | |
| - status | |
| - error | |
| - errorCode | |
| - clientId | |
| - requestId | |
| - serviceName | |
| - userId | |
| properties: | |
| status: | |
| type: string | |
| enum: [error] | |
| error: | |
| type: string | |
| description: Человекочитаемое описание ошибки | |
| errorCode: | |
| type: string | |
| enum: | |
| - GSI_API_ERROR | |
| - GSI_NO_DATA | |
| - LIVENESS_NOT_ALIVE | |
| - LIVENESS_NO_AVERAGE | |
| - LIVENESS_NO_BEST_FACE | |
| - LIVENESS_NO_NORMALIZED | |
| - LIVENESS_SCORE_TOO_LOW | |
| - LIVENESS_MULTIPLE_FACES | |
| - EYES_CLOSED_OR_HIDDEN | |
| - HEAD_TURNED | |
| - GSI_PHOTO_MULTIPLE_FACES | |
| - GSI_PHOTO_NO_FACE | |
| - FACEIDS_API_ERROR | |
| - SIMILARITY_SCORE_TOO_LOW | |
| - PASSPORT_INVALID | |
| - DOCUMENT_EXPIRED | |
| - INVALID_PAYLOAD | |
| - UNKNOWN_ERROR | |
| clientId: | |
| type: string | |
| requestId: | |
| type: string | |
| serviceName: | |
| type: string | |
| userId: | |
| type: string | |
| ``` | |
| ### 5.7 Структура запроса | |
| | Поле | Тип | Обязательное | Описание | | |
| |------|-----|--------------|----------| | |
| | `serviceName` | string | Да | Название сервиса | | |
| | `clientId` | string | Да | Идентификатор клиента | | |
| | `userId` | string | Да | Идентификатор пользователя | | |
| | `token` | string | Да | Токен авторизации (JWT) | | |
| | `requestId` | string | Да | Уникальный идентификатор запроса | | |
| | `birth_date` | string | Да | Дата рождения (DD.MM.YYYY) | | |
| | `doc_number` | string | Да | Номер документа | | |
| | `doc_seria` | string | Да | Серия документа | | |
| | `doc_pinfl` | string | Да | ПИНФЛ | | |
| | `video` | binary/base64 | Да | Видео для liveness | | |
| ### 5.8 Пример успешного ответа (HTTP 200) | |
| ```json | |
| { | |
| "status": "ok", | |
| "clientId": "unknown", | |
| "requestId": "cecb73b4-0035-47bf-9cfe-cc3c8aadff41", | |
| "userId": "unknown", | |
| "serviceName": "unknown", | |
| "data": { | |
| "id": 3, | |
| "name": "PINFL=31006996530022 BIRTH=10.06.1999", | |
| "pin": "", | |
| "sex": "1", | |
| "namecyr": "ПЕТР", | |
| "namelat": "PETR", | |
| "pinpps_1": "31006996530022", | |
| "doc_pinfl": "31006996530022", | |
| "birth_date": "10.06.1999", | |
| "birthplace": "ROSSIYA FEDERASIYASI", | |
| "livestatus": "1", | |
| "surnamecyr": "ТРИПОЛЬСКИЙ", | |
| "surnamelat": "TRIPOLSKIY", | |
| "citizenship": "РОССИЙСКАЯ ФЕДЕРАЦИЯ", | |
| "patronymcyr": "ПЕТРОВИЧ", | |
| "patronymlat": "PETROVICH", | |
| "citizenshipid": "192", | |
| "current_pinpp": "31006996530022", | |
| "current_document": "763490940", | |
| "foreign_documents_1_document": "763490940", | |
| "foreign_documents_1_datebegin": "2020-10-01", | |
| "foreign_documents_1_docgiveplace": "РОССИЯ", | |
| "foreign_documents_1_citizenshipid": "192", | |
| "createdDate": "2025-12-05T02:13:30.350985+05:00", | |
| "modifiedDate": "2025-12-05T02:13:30.656538+05:00", | |
| "photo": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQN...", | |
| "signature": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJib2R5Ijoie1wiaWRcIjozLFwibmFtZV...", | |
| "capture": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQD...", | |
| "score": 0.8402466, | |
| "createdAt": "2025-12-30T12:54:17.348Z" | |
| } | |
| } | |
| ``` | |
| ### 5.9 Пример ответа с ошибкой (HTTP 403) | |
| ```json | |
| { | |
| "status": "error", | |
| "error": "Низкое сходство с фото из ГЦП", | |
| "errorCode": "SIMILARITY_SCORE_TOO_LOW", | |
| "clientId": "unknown", | |
| "requestId": "4fd3acd4-cc20-40ab-9257-a62cffdb611b", | |
| "serviceName": "unknown", | |
| "userId": "unknown" | |
| } | |
| ``` | |
| ### 5.10 Описание полей успешного ответа | |
| | Поле | Тип | Описание | | |
| |------|-----|----------| | |
| | `status` | string | Статус операции: `ok` | | |
| | `clientId` | string | Идентификатор клиента из запроса | | |
| | `requestId` | string | Идентификатор запроса | | |
| | `userId` | string | Идентификатор пользователя из запроса | | |
| | `serviceName` | string | Название сервиса из запроса | | |
| | `data.pin` | string | ПИНФЛ | | |
| | `data.doc_pinfl` | string | ПИНФЛ из документа | | |
| | `data.namelat` | string | Имя (латиница) | | |
| | `data.surnamelat` | string | Фамилия (латиница) | | |
| | `data.patronymlat` | string | Отчество (латиница) | | |
| | `data.namecyr` | string | Имя (кириллица) | | |
| | `data.surnamecyr` | string | Фамилия (кириллица) | | |
| | `data.patronymcyr` | string | Отчество (кириллица) | | |
| | `data.birth_date` | string | Дата рождения | | |
| | `data.birthplace` | string | Место рождения | | |
| | `data.citizenship` | string | Гражданство | | |
| | `data.livestatus` | string | Статус: `1` — жив | | |
| | `data.current_pinpp` | string | Текущий ПИНФЛ | | |
| | `data.current_document` | string | Номер текущего документа | | |
| | `data.photo` | string | Эталонное фото из ГЦП (base64 JPEG) | | |
| | `data.capture` | string | Кадр из видео (base64 JPEG) | | |
| | `data.signature` | string | JWT-подпись ответа (HS256) | | |
| | `data.score` | number | Коэффициент сходства (0.0 — 1.0) | | |
| | `data.createdAt` | string | Дата и время верификации (ISO 8601) | | |
| ### 5.11 Структура JWT-подписи (signature) | |
| JWT-подпись содержит следующие claims: | |
| | Claim | Описание | | |
| |-------|----------| | |
| | `body` | JSON-строка с данными гражданина | | |
| | `score` | Коэффициент сходства | | |
| | `serviceName` | Название сервиса | | |
| | `clientId` | Идентификатор клиента | | |
| | `userId` | Идентификатор пользователя | | |
| | `requestId` | Идентификатор запроса | | |
| | `photoHash` | SHA-256 хеш эталонного фото | | |
| | `captureHash` | SHA-256 хеш кадра из видео | | |
| | `videoHash` | SHA-256 хеш видео | | |
| | `token` | Токен из запроса | | |
| | `issuerName` | Имя банка-эмитента | | |
| | `createdAt` | Дата создания | | |
| | `iat` | Issued At (Unix timestamp) | | |
| | `exp` | Expiration (Unix timestamp) | | |
| ### 5.12 История паспортов и полная схема данных гражданина | |
| При верификации по ПИНФЛ возвращается расширенная информация о гражданине, включая историю документов. Вложенные массивы (до 5 документов) представлены в виде плоской структуры с суффиксом `_N_`, где N — порядковый номер элемента (1-5). | |
| SDK автоматически преобразует плоские поля в массивы для удобства работы. | |
| #### 5.12.1 Полная схема данных (HumanModel) | |
| ```typescript | |
| /** | |
| * Citizen Data Schema - Flattened Document Interface | |
| * Combined schema from passport and PINFL service responses | |
| * All fields are optional as not all appear in every response variant | |
| * Nested arrays are flattened: documents[0].field -> documents_1_field | |
| */ | |
| export interface HumanModel { | |
| // === Уникальный идентификатор записи === | |
| name: string; // Внутренний идентификатор записи в системе | |
| // === Идентификация === | |
| pin: string; // ПИНФЛ | |
| current_pinpp: string; // Текущий ПИНФЛ (может отличаться при смене) | |
| pinpps_1: string; // Первичный ПИНФЛ | |
| pinpps_2: string; // Альтернативный ПИНФЛ #2 | |
| pinpps_3: string; // Альтернативный ПИНФЛ #3 | |
| pinpps_4: string; // Альтернативный ПИНФЛ #4 | |
| document: string; // Номер документа | |
| current_document: string; // Номер текущего документа | |
| transaction_id: string; // ID транзакции в ГЦП | |
| // === ФИО (латиница, стандартные поля) === | |
| first_name: string; // Имя | |
| last_name: string; // Фамилия | |
| patronym: string; // Отчество | |
| surname: string; // Фамилия (альтернативное поле) | |
| givenname: string; // Имя (альтернативное поле) | |
| // === ФИО (варианты написания) === | |
| namelat: string; // Имя (латиница, транслит) | |
| surnamelat: string; // Фамилия (латиница, транслит) | |
| patronymlat: string; // Отчество (латиница, транслит) | |
| namecyr: string; // Имя (кириллица) | |
| surnamecyr: string; // Фамилия (кириллица) | |
| patronymcyr: string; // Отчество (кириллица) | |
| engname: string; // Имя (английский) | |
| engsurname: string; // Фамилия (английский) | |
| engpatronym: string; // Отчество (английский) | |
| patronym_en: string; // Отчество (английский, альтернативное) | |
| // === Текущий документ === | |
| doc_pinfl: string; // ПИНФЛ в документе | |
| doc_seria: string; // Серия документа | |
| doc_number: string; // Номер документа | |
| date_issue: string; // Дата выдачи | |
| date_expiry: string; // Дата окончания срока действия | |
| give_place: string; // Код места выдачи | |
| give_place_name: string; // Название места выдачи | |
| // === История документов (до 5 записей) === | |
| // Формат: documents_N_field, где N = 1-5 | |
| documents_1_document: string; // Номер документа #1 | |
| documents_1_type: string; // Тип документа | |
| documents_1_datebegin: string; // Дата выдачи | |
| documents_1_dateend: string; // Дата окончания | |
| documents_1_docgiveplace: string; // Место выдачи | |
| documents_1_docgiveplaceid: string; // ID места выдачи | |
| documents_1_status: string; // Статус документа | |
| documents_2_document: string; | |
| documents_2_type: string; | |
| documents_2_datebegin: string; | |
| documents_2_dateend: string; | |
| documents_2_docgiveplace: string; | |
| documents_2_docgiveplaceid: string; | |
| documents_2_status: string; | |
| documents_3_document: string; | |
| documents_3_type: string; | |
| documents_3_datebegin: string; | |
| documents_3_dateend: string; | |
| documents_3_docgiveplace: string; | |
| documents_3_docgiveplaceid: string; | |
| documents_3_status: string; | |
| documents_4_document: string; | |
| documents_4_type: string; | |
| documents_4_datebegin: string; | |
| documents_4_dateend: string; | |
| documents_4_docgiveplace: string; | |
| documents_4_docgiveplaceid: string; | |
| documents_4_status: string; | |
| documents_5_document: string; | |
| documents_5_type: string; | |
| documents_5_datebegin: string; | |
| documents_5_dateend: string; | |
| documents_5_docgiveplace: string; | |
| documents_5_docgiveplaceid: string; | |
| documents_5_status: string; | |
| // === Иностранные документы (для неграждан, до 5 записей) === | |
| // Формат: foreign_documents_N_field, где N = 1-5 | |
| foreign_documents_1_document: string; // Номер иностранного документа #1 | |
| foreign_documents_1_citizenshipid: string; // ID гражданства | |
| foreign_documents_1_datebegin: string; // Дата выдачи | |
| foreign_documents_1_dateend: string; // Дата окончания | |
| foreign_documents_1_docgiveplace: string; // Место выдачи | |
| foreign_documents_2_document: string; | |
| foreign_documents_2_citizenshipid: string; | |
| foreign_documents_2_datebegin: string; | |
| foreign_documents_2_dateend: string; | |
| foreign_documents_2_docgiveplace: string; | |
| foreign_documents_3_document: string; | |
| foreign_documents_3_citizenshipid: string; | |
| foreign_documents_3_datebegin: string; | |
| foreign_documents_3_dateend: string; | |
| foreign_documents_3_docgiveplace: string; | |
| foreign_documents_4_document: string; | |
| foreign_documents_4_citizenshipid: string; | |
| foreign_documents_4_datebegin: string; | |
| foreign_documents_4_dateend: string; | |
| foreign_documents_4_docgiveplace: string; | |
| foreign_documents_5_document: string; | |
| foreign_documents_5_citizenshipid: string; | |
| foreign_documents_5_datebegin: string; | |
| foreign_documents_5_dateend: string; | |
| foreign_documents_5_docgiveplace: string; | |
| // === Информация о рождении === | |
| birth_date: string; // Дата рождения (DD.MM.YYYY) | |
| birth_place: string; // Место рождения | |
| birthplace: string; // Место рождения (альтернативное поле) | |
| birth_country: string; // Код страны рождения | |
| birth_country_name: string; // Название страны рождения | |
| birthcountry: string; // Страна рождения (альтернативное) | |
| birthcountryid: string; // ID страны рождения | |
| // === Гражданство и национальность === | |
| citizenship: string; // Гражданство (название) | |
| citizenship_name: string; // Гражданство (полное название) | |
| citizenshipid: string; // ID гражданства | |
| nationality: string; // Национальность (код) | |
| nationality_name: string; // Национальность (название) | |
| nationalityid: string; // ID национальности | |
| // === Пол и статус === | |
| gender: string; // Пол (1 - мужской, 2 - женский) | |
| gender_name: string; // Название пола | |
| sex: string; // Пол (альтернативное поле) | |
| subject_state: string; // Код состояния субъекта | |
| subject_state_name: string; // Название состояния субъекта | |
| livestatus: string; // Статус жизни (1 - жив) | |
| } | |
| ``` | |
| #### 5.12.2 Преобразование плоских полей в массивы | |
| SDK предоставляет функцию для преобразования плоских полей документов в массивы: | |
| ```typescript | |
| interface DocumentRecord { | |
| document: string; | |
| type?: string; | |
| datebegin: string; | |
| dateend?: string; | |
| docgiveplace: string; | |
| docgiveplaceid?: string; | |
| status?: string; | |
| } | |
| interface ForeignDocumentRecord { | |
| document: string; | |
| citizenshipid: string; | |
| datebegin: string; | |
| dateend?: string; | |
| docgiveplace: string; | |
| } | |
| function extractDocuments(data: HumanModel): DocumentRecord[] { | |
| const documents: DocumentRecord[] = []; | |
| for (let i = 1; i <= 5; i++) { | |
| const doc = data[`documents_${i}_document`]; | |
| if (doc) { | |
| documents.push({ | |
| document: doc, | |
| type: data[`documents_${i}_type`], | |
| datebegin: data[`documents_${i}_datebegin`], | |
| dateend: data[`documents_${i}_dateend`], | |
| docgiveplace: data[`documents_${i}_docgiveplace`], | |
| docgiveplaceid: data[`documents_${i}_docgiveplaceid`], | |
| status: data[`documents_${i}_status`], | |
| }); | |
| } | |
| } | |
| return documents; | |
| } | |
| function extractForeignDocuments(data: HumanModel): ForeignDocumentRecord[] { | |
| const documents: ForeignDocumentRecord[] = []; | |
| for (let i = 1; i <= 5; i++) { | |
| const doc = data[`foreign_documents_${i}_document`]; | |
| if (doc) { | |
| documents.push({ | |
| document: doc, | |
| citizenshipid: data[`foreign_documents_${i}_citizenshipid`], | |
| datebegin: data[`foreign_documents_${i}_datebegin`], | |
| dateend: data[`foreign_documents_${i}_dateend`], | |
| docgiveplace: data[`foreign_documents_${i}_docgiveplace`], | |
| }); | |
| } | |
| } | |
| return documents; | |
| } | |
| ``` | |
| #### 5.12.3 Пример данных с историей документов | |
| ```json | |
| { | |
| "status": "ok", | |
| "data": { | |
| "pin": "31006996530022", | |
| "current_pinpp": "31006996530022", | |
| "namelat": "PETR", | |
| "surnamelat": "TRIPOLSKIY", | |
| "patronymlat": "PETROVICH", | |
| "namecyr": "ПЕТР", | |
| "surnamecyr": "ТРИПОЛЬСКИЙ", | |
| "patronymcyr": "ПЕТРОВИЧ", | |
| "birth_date": "10.06.1999", | |
| "birthplace": "ROSSIYA FEDERASIYASI", | |
| "citizenship": "РОССИЙСКАЯ ФЕДЕРАЦИЯ", | |
| "citizenshipid": "192", | |
| "livestatus": "1", | |
| "current_document": "763490940", | |
| "foreign_documents_1_document": "763490940", | |
| "foreign_documents_1_datebegin": "2020-10-01", | |
| "foreign_documents_1_docgiveplace": "РОССИЯ", | |
| "foreign_documents_1_citizenshipid": "192", | |
| "foreign_documents_2_document": "", | |
| "foreign_documents_2_datebegin": "", | |
| "foreign_documents_2_docgiveplace": "", | |
| "foreign_documents_2_citizenshipid": "" | |
| } | |
| } | |
| ``` | |
| #### 5.12.4 Описание полей истории документов | |
| | Поле | Описание | | |
| |------|----------| | |
| | `documents_N_document` | Номер N-го документа в истории | | |
| | `documents_N_type` | Тип документа (паспорт, ID-карта и т.д.) | | |
| | `documents_N_datebegin` | Дата выдачи документа | | |
| | `documents_N_dateend` | Дата окончания срока действия | | |
| | `documents_N_docgiveplace` | Место выдачи документа | | |
| | `documents_N_docgiveplaceid` | ID места выдачи | | |
| | `documents_N_status` | Статус документа (действителен/недействителен) | | |
| | Поле | Описание | | |
| |------|----------| | |
| | `foreign_documents_N_document` | Номер N-го иностранного документа | | |
| | `foreign_documents_N_citizenshipid` | ID гражданства владельца документа | | |
| | `foreign_documents_N_datebegin` | Дата выдачи документа | | |
| | `foreign_documents_N_dateend` | Дата окончания срока действия | | |
| | `foreign_documents_N_docgiveplace` | Место выдачи (страна) | | |
| Где N — порядковый номер от 1 до 5. Всего может быть до 5 записей в каждой категории | |
| --- | |
| ## 6. Интеграция с mTLS | |
| ### 6.1 Управление сертификатами | |
| Создание и управление клиентскими mTLS-сертификатами находится в зоне ответственности банка. Банк выступает в роли Certificate Authority (CA) для своих клиентов. ГЦИ не участвует в процессе выпуска сертификатов, однако требует прохождения аудита информационной безопасности. | |
| ```mermaid | |
| flowchart TB | |
| subgraph bank["Инфраструктура банка"] | |
| CA["CA банка"] | |
| AUTH["Auth Provider"] | |
| PROXY["mTLS Proxy"] | |
| end | |
| subgraph client["Клиентское устройство"] | |
| APP["Мобильное приложение"] | |
| SDK["GSI SDK"] | |
| end | |
| APP -->|"1. Запрос сертификата"| AUTH | |
| AUTH -->|"2. Генерация сертификата"| CA | |
| CA -->|"3. P12 (base64) на 20 мин"| AUTH | |
| AUTH -->|"4. Сертификат + accessToken"| APP | |
| APP -->|"5. Передача в SDK"| SDK | |
| SDK -->|"6. Запрос верификации"| PROXY | |
| style bank fill:#e3f2fd | |
| style client fill:#e8f5e9 | |
| ``` | |
| ### 6.2 Требования к сертификатам | |
| | Параметр | Рекомендация | | |
| |----------|--------------| | |
| | Срок действия | Не более 20 минут | | |
| | Алгоритм | RSA 2048+ или ECDSA P-256+ (чем выше, тем лучше) | | |
| | Формат | P12 (PKCS#12) — рекомендуется | | |
| | Хранение | Динамическая загрузка, запрещено вшивать в APK/IPA | | |
| Рекомендуется выпускать отдельный сертификат для каждого пользователя со следующими данными в полях сертификата: | |
| | Поле | Описание | | |
| |------|----------| | |
| | `CN` | ФИО или идентификатор пользователя | | |
| | `O` | Название банка | | |
| | `OU` | Подразделение | | |
| | `C` | UZ | | |
| | `serialNumber` | Номер паспорта или телефона | | |
| ### 6.3 Интеграция сертификатов в SDK | |
| **Android — P12:** | |
| ```kotlin | |
| FaceDetectionSdk.Builder(context) | |
| .setClientP12Base64(p12CertificateBase64, p12Password) | |
| .setEnableMutualSSL(true) | |
| .build() | |
| ``` | |
| **Android — раздельные сертификаты:** | |
| ```kotlin | |
| FaceDetectionSdk.Builder(context) | |
| .setClientPrivateKeyBase64(privateKeyBase64) | |
| .setClientCertificateChainBase64(certificateChainBase64) | |
| .setEnableMutualSSL(true) | |
| .build() | |
| ``` | |
| **iOS:** | |
| ```swift | |
| let sslConfig = SSLConfig( | |
| clientCertificateBase64: "BASE64_P12", | |
| clientCertificatePassword: "password", | |
| enableMutualSSL: true | |
| ) | |
| ``` | |
| **Flutter:** | |
| ```dart | |
| final config = SdkConfig( | |
| enableMutualSSL: true, | |
| clientCertificateBase64: 'BASE64_P12', | |
| clientCertificatePassword: 'password', | |
| ); | |
| ``` | |
| ### 6.4 Ротация и отзыв сертификатов | |
| При сроке действия сертификата 20 минут механизм отзыва (CRL) не является обязательным. При необходимости отзыв может быть реализован на уровне mTLS-прокси банка. В комплект поставки включён пример кода на dotnet-script для создания и ротации сертификатов. | |
| Если сертификат истечёт во время сессии (например, пользователь выключил экран на 20+ минут), последующие верификации завершатся сетевой ошибкой. | |
| --- | |
| ## 7. Механизм защиты от MITM-атак | |
| ### 7.1 Двойная JWT-защита | |
| Для исключения атак типа CWE-639 (Authorization Bypass Through User-Controlled Key) система использует два уровня JWT-токенов. | |
| **accessToken (формирует банк):** | |
| Банк кодирует `verificationId` в public claims токена. Это исключает передачу идентификатора в открытом виде и защищает от MITM-атак на стороне запроса. | |
| ```json | |
| { | |
| "sub": "user_id", | |
| "verificationId": "encrypted_or_hashed_id", | |
| "iat": 1234567890, | |
| "exp": 1234569090 | |
| } | |
| ``` | |
| **signature (формирует ГЦИ):** | |
| Ответ сервера ГЦИ подписывается JWT с алгоритмом HS256. Для каждого банка используется индивидуальный секретный ключ, так как банки работают с разными репликами. | |
| Ключ подписи передаётся представителю банка лично на физическом носителе (флешка). | |
| ### 7.2 Auth Provider банка | |
| Для реализации двойной JWT-защиты банку необходимо создать Auth Provider со следующей логикой: | |
| ```mermaid | |
| sequenceDiagram | |
| participant App as Приложение банка | |
| participant Auth as Auth Provider | |
| participant DB as База данных | |
| App->>Auth: Запрос accessToken | |
| Auth->>Auth: Генерация verificationId | |
| Auth->>DB: Сохранение verificationId + метаданные | |
| Auth->>Auth: Формирование JWT с verificationId | |
| Auth-->>App: accessToken (JWT) | |
| Note over App,Auth: verificationId недоступен<br/>в открытом виде | |
| ``` | |
| Auth Provider собирает необходимые данные в закрытом виде на backend и возвращает уникальную строку (JWT) в режиме только на чтение. | |
| --- | |
| ## 8. Настройка инфраструктуры банка | |
| ### 8.1 Пример Mutual SSL Proxy на C# | |
| В комплект поставки включён пример прокси-сервера на C# с использованием dotnet-script. Реализация использует исключительно стандартную библиотеку .NET без внешних зависимостей. | |
| ```csharp | |
| #!/usr/bin/env dotnet-script | |
| using System.Net; | |
| using System.Net.Security; | |
| using System.Security.Cryptography.X509Certificates; | |
| using System.Text; | |
| using System.Text.Json; | |
| var serverCert = new X509Certificate2("server.pfx", "password"); | |
| var caCert = new X509Certificate2("ca.crt"); | |
| var listener = new HttpListener(); | |
| listener.Prefixes.Add("https://+:8443/"); | |
| listener.Start(); | |
| Console.WriteLine("mTLS Proxy started on port 8443"); | |
| while (true) | |
| { | |
| var context = await listener.GetContextAsync(); | |
| _ = HandleRequest(context); | |
| } | |
| async Task HandleRequest(HttpListenerContext context) | |
| { | |
| try | |
| { | |
| var clientCert = context.Request.GetClientCertificate(); | |
| if (clientCert == null || !ValidateCertificate(clientCert)) | |
| { | |
| context.Response.StatusCode = 403; | |
| context.Response.Close(); | |
| return; | |
| } | |
| // Логирование запроса сертификата | |
| var certInfo = new { | |
| Subject = clientCert.Subject, | |
| SerialNumber = clientCert.SerialNumber, | |
| NotAfter = clientCert.NotAfter, | |
| Thumbprint = clientCert.Thumbprint | |
| }; | |
| Console.WriteLine($"Client cert: {JsonSerializer.Serialize(certInfo)}"); | |
| // Проксирование запроса к ГЦИ через IPSec | |
| var gciResponse = await ForwardToGci(context.Request); | |
| // Извлечение verificationId из accessToken | |
| var accessToken = context.Request.Headers["Authorization"]? | |
| .Replace("Bearer ", ""); | |
| var verificationId = ExtractVerificationId(accessToken); | |
| // Отправка webhook (опционально) | |
| if (!string.IsNullOrEmpty(verificationId)) | |
| { | |
| await SendWebhook(verificationId, gciResponse); | |
| } | |
| // Возврат ответа (с пустыми строками в webhook-режиме) | |
| var responseBytes = Encoding.UTF8.GetBytes(gciResponse); | |
| context.Response.ContentType = "application/json"; | |
| await context.Response.OutputStream.WriteAsync(responseBytes); | |
| context.Response.Close(); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Error: {ex.Message}"); | |
| context.Response.StatusCode = 500; | |
| context.Response.Close(); | |
| } | |
| } | |
| bool ValidateCertificate(X509Certificate2 cert) | |
| { | |
| // Проверка срока действия | |
| if (DateTime.Now > cert.NotAfter || DateTime.Now < cert.NotBefore) | |
| return false; | |
| // Проверка цепочки сертификатов | |
| var chain = new X509Chain(); | |
| chain.ChainPolicy.ExtraStore.Add(caCert); | |
| chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; | |
| return chain.Build(cert); | |
| } | |
| async Task<string> ForwardToGci(HttpListenerRequest request) | |
| { | |
| // Реализация проксирования через IPSec туннель | |
| // ... | |
| return "{}"; | |
| } | |
| string ExtractVerificationId(string jwt) | |
| { | |
| // Декодирование JWT и извлечение verificationId | |
| // ... | |
| return ""; | |
| } | |
| async Task SendWebhook(string verificationId, string data) | |
| { | |
| // Отправка данных на webhook банка | |
| // ... | |
| } | |
| ``` | |
| ### 8.2 Базовая конфигурация IPSec | |
| Настройка IPSec-туннеля находится в зоне ответственности ГЦИ. Рекомендуемые параметры: | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Протокол | IKEv2 | | |
| | Шифрование | AES-256-GCM | | |
| | Аутентификация | SHA-384 | | |
| | DH Group | 20 (NIST P-384) или выше | | |
| | ESP | AES-256-GCM | | |
| Чем выше уровень защиты, тем лучше — это исключает атаки типа «intercept now, hack later». | |
| --- | |
| ## 9. Механизмы безопасности SDK | |
| ### 9.1 Защита от root/jailbreak | |
| | Платформа | Реализация | Поведение | | |
| |-----------|------------|-----------| | |
| | Android | Библиотека `scottyab.rootbeer` | Отказ в работе в модальном окне | | |
| | iOS | Проверка на Cydia.app и специфичные бинарники | Отказ в работе в модальном окне | | |
| Проверяется наличие: Magisk, SuperSU, системных путей root-доступа, модифицированных системных файлов. | |
| ### 9.2 Защита от эмуляторов | |
| | Платформа | Реализация | Поведение | | |
| |-----------|------------|-----------| | |
| | Android | Проверка характеристик эмулятора | Отказ в работе в модальном окне | | |
| | iOS | Приложение не запустится (нет доступа к камере) | — | | |
| ### 9.3 Защита от отладки и инструментации | |
| | Угроза | Платформа | Поведение | | |
| |--------|-----------|-----------| | |
| | Подключение отладчика | Android | Отказ в работе в модальном окне | | |
| | Frida | Android | Отказ в работе в модальном окне | | |
| ### 9.4 Дополнительные меры | |
| - SDK не сохраняет промежуточные данные на диск | |
| - Видео и результаты верификации находятся только в оперативной памяти | |
| - Логирование принудительно выключено для предотвращения утечки данных | |
| - Код SDK обфусцирован | |
| - Биометрические данные локально не хранятся — система работает stateless | |
| --- | |
| ## 10. Зоны ответственности и аудит | |
| ### 10.1 Разграничение ответственности | |
| | Компонент | Ответственный | | |
| |-----------|---------------| | |
| | GSI Mobile SDK | ГЦИ | | |
| | IPSec-туннель | ГЦИ | | |
| | mTLS Proxy | Банк | | |
| | CA для клиентских сертификатов | Банк | | |
| | Auth Provider | Банк | | |
| | Webhook endpoint | Банк | | |
| | Rate limiter | Банк | | |
| ### 10.2 Рекомендация по аудиту | |
| SDK предоставляет набор инструментов, который делает решение безопасным при правильном использовании. Однако банк может по халатности использовать инструменты небезопасным образом (вшить сертификаты в APK, использовать длительный срок действия сертификатов, не реализовать двойную JWT-защиту). | |
| **Рекомендуется проведение аудита информационной безопасности со стороны ГЦИ перед запуском в production.** | |
| Аудит должен включать проверку: механизма выдачи сертификатов, срока действия сертификатов, реализации Auth Provider, конфигурации mTLS Proxy, наличия rate limiter. | |
| --- | |
| ## 11. Требования к оборудованию | |
| | Параметр | Требование | | |
| |----------|------------| | |
| | Камера | Фронтальная с автофокусом | | |
| | FPS | > 5 кадров/сек | | |
| | Разрешение | Желательно Full HD | | |
| | RAM | 2 ГБ свободно | | |
| | Процессор | ARM64 | | |
| | Планшеты | Поддерживаются | | |
| IR-камеры не поддерживаются — требуется цветное видео. | |
| --- | |
| ## 12. Сетевые требования | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Bandwidth | ~5 МБ на верификацию × до 3 попыток | | |
| | Timeout | 30 секунд на операцию | | |
| | Rate limit | Не более 1 запроса в 15 секунд на пользователя | | |
| Rate limiter должен быть установлен банком между SDK и mTLS Proxy. | |
| --- | |
| ## 13. Установка SDK | |
| ### 13.1 Android | |
| Добавить в `build.gradle.kts`: | |
| ```gradle | |
| repositories { | |
| flatDir { | |
| dirs("libs") | |
| } | |
| } | |
| dependencies { | |
| implementation(kotlin("reflect")) | |
| implementation(files("libs/facedetectsdk.aar")) | |
| implementation("androidx.camera:camera-core:1.4.2") | |
| implementation("androidx.camera:camera-lifecycle:1.4.2") | |
| implementation("androidx.camera:camera-video:1.4.2") | |
| implementation("androidx.camera:camera-view:1.4.2") | |
| implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") | |
| implementation("com.google.mlkit:face-detection:16.1.7") | |
| implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") | |
| implementation("com.scottyab:rootbeer-lib:0.1.1") | |
| implementation("com.googlecode.mp4parser:isoparser:1.1.22") | |
| implementation("androidx.camera:camera-camera2:1.3.0") | |
| } | |
| ``` | |
| Добавить в `AndroidManifest.xml`: | |
| ```xml | |
| <uses-permission android:name="android.permission.CAMERA"/> | |
| <uses-permission android:name="android.permission.INTERNET"/> | |
| <uses-permission android:name="android.permission.WAKE_LOCK"/> | |
| ``` | |
| ### 13.4 ProGuard/R8 правила для Android | |
| При использовании ProGuard или R8 необходимо добавить правила, исключающие конфликты со сжатием AAR. Добавить в `proguard-rules.pro`: | |
| ```proguard | |
| # ============================================================================= | |
| # GSI FaceDetect SDK | |
| # ============================================================================= | |
| # Основные классы SDK | |
| -keep public class uz.detectface.facedetectsdk.FaceDetectionSdk { | |
| public <methods>; | |
| public <fields>; | |
| } | |
| -keep public class uz.detectface.facedetectsdk.FaceDetectionSdk$Builder { | |
| public <init>(android.content.Context); | |
| public <methods>; | |
| } | |
| -keep public class uz.detectface.facedetectsdk.FaceDetectionSdk$Companion { | |
| public <methods>; | |
| } | |
| -keepclassmembers class uz.detectface.facedetectsdk.FaceDetectionSdk$Companion { | |
| public static ** getResultFromIntent(...); | |
| public static ** getResultFromIntent$default(...); | |
| } | |
| # Модели данных | |
| -keep class uz.detectface.facedetectsdk.model.** { *; } | |
| # Enum-классы | |
| -keep public class uz.detectface.facedetectsdk.ResultFormat { | |
| public <init>(...); | |
| public <methods>; | |
| public <fields>; | |
| public **[] values(); | |
| public ** valueOf(java.lang.String); | |
| } | |
| -keep public class uz.detectface.facedetectsdk.model.OverlayType { | |
| public <init>(...); | |
| public <methods>; | |
| public <fields>; | |
| public **[] values(); | |
| public ** valueOf(java.lang.String); | |
| } | |
| # Обфускация внутренних классов | |
| -repackageclasses 'uz.detectface.internal.a' | |
| -allowaccessmodification | |
| -overloadaggressively | |
| -keep,allowobfuscation class uz.detectface.facedetectsdk.** { *; } | |
| -keep,allowshrinking class uz.detectface.facedetectsdk.model.OverlayType { *; } | |
| -keep,allowshrinking class uz.detectface.facedetectsdk.ResultFormat { *; } | |
| -keep,allowobfuscation class uz.detectface.facedetectsdk.*$* { *; } | |
| -keep,allowobfuscation class uz.detectface.facedetectsdk.mapping.** { *; } | |
| # ============================================================================= | |
| # Kotlinx Serialization | |
| # ============================================================================= | |
| -keepattributes *Annotation*, InnerClasses | |
| -dontnote kotlinx.serialization.AnnotationsKt | |
| -keepclassmembers class uz.detectface.facedetectsdk.** { | |
| *** Companion; | |
| } | |
| -keepclasseswithmembers class uz.detectface.facedetectsdk.** { | |
| kotlinx.serialization.KSerializer serializer(...); | |
| } | |
| -keep,includedescriptorclasses class uz.detectface.facedetectsdk.**$$serializer { *; } | |
| -keepclassmembers @kotlinx.serialization.Serializable class uz.detectface.facedetectsdk.** { | |
| @kotlinx.serialization.SerialName <fields>; | |
| } | |
| -keep class kotlinx.serialization.** { *; } | |
| -keep interface kotlinx.serialization.** { *; } | |
| # ============================================================================= | |
| # Сторонние зависимости | |
| # ============================================================================= | |
| # RootBeer (проверка root) | |
| -keep,allowobfuscation class com.scottyab.rootbeer.** { *; } | |
| -dontwarn com.scottyab.rootbeer.** | |
| # CameraX | |
| -keep class androidx.camera.core.Preview { *; } | |
| -keep class androidx.camera.view.PreviewView { *; } | |
| -keep class androidx.camera.lifecycle.ProcessCameraProvider { *; } | |
| -keep class androidx.camera.core.Camera { *; } | |
| -keep class androidx.camera.core.CameraSelector { *; } | |
| -keep class androidx.camera.core.ImageCapture { *; } | |
| -keep class androidx.camera.core.ImageAnalysis { *; } | |
| -keep,allowobfuscation class androidx.camera.** { *; } | |
| -dontwarn androidx.camera.** | |
| # ML Kit Face Detection | |
| -keep class com.google.mlkit.vision.face.FaceDetector { *; } | |
| -keep class com.google.mlkit.vision.face.FaceDetection { *; } | |
| -keep class com.google.mlkit.vision.face.Face { *; } | |
| -keep class com.google.mlkit.vision.face.FaceLandmark { *; } | |
| -keep class com.google.mlkit.vision.face.FaceDetectorOptions { *; } | |
| -keep class com.google.mlkit.vision.face.FaceDetectorOptions$Builder { *; } | |
| -keep class com.google.mlkit.vision.common.InputImage { *; } | |
| -keep,allowobfuscation class com.google.mlkit.** { *; } | |
| -dontwarn com.google.mlkit.** | |
| # ============================================================================= | |
| # Kotlin | |
| # ============================================================================= | |
| -keep class kotlin.** { *; } | |
| -keep class kotlin.Metadata { *; } | |
| -dontwarn kotlin.** | |
| -keepclassmembernames class kotlinx.** { *; } | |
| -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} | |
| -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} | |
| # ============================================================================= | |
| # SSL/TLS (для mTLS) | |
| # ============================================================================= | |
| -keep class javax.net.ssl.** { *; } | |
| -keep class java.security.** { *; } | |
| -keep class java.security.cert.** { *; } | |
| -keep class java.security.spec.** { *; } | |
| -keep class java.security.KeyStore { *; } | |
| -keep class java.security.KeyStore$* { *; } | |
| -keep class javax.net.ssl.SSLContext { *; } | |
| -keep class javax.net.ssl.KeyManagerFactory { *; } | |
| -keep class javax.net.ssl.TrustManagerFactory { *; } | |
| -keep class javax.net.ssl.KeyManager { *; } | |
| -keep class javax.net.ssl.X509KeyManager { *; } | |
| -keep class javax.net.ssl.TrustManager { *; } | |
| -keep class javax.net.ssl.X509TrustManager { *; } | |
| -keep class java.security.Provider { *; } | |
| -keep class java.security.Security { *; } | |
| -keep class java.security.cert.Certificate { *; } | |
| -keep class java.security.cert.X509Certificate { *; } | |
| -keep class java.security.cert.CertificateFactory { *; } | |
| -keep class java.security.KeyFactory { *; } | |
| -keep class java.security.PrivateKey { *; } | |
| -keep class java.security.PublicKey { *; } | |
| # ============================================================================= | |
| # Оптимизация | |
| # ============================================================================= | |
| -optimizationpasses 5 | |
| -dontusemixedcaseclassnames | |
| -dontskipnonpubliclibraryclasses | |
| -verbose | |
| # Удаление логов в release | |
| -assumenosideeffects class android.util.Log { | |
| public static *** d(...); | |
| public static *** v(...); | |
| public static *** i(...); | |
| } | |
| # Сохранение информации для stack traces | |
| -renamesourcefileattribute SourceFile | |
| -keepattributes SourceFile,LineNumberTable | |
| -keepattributes LocalVariableTable,LocalVariableTypeTable | |
| -keepattributes Signature | |
| -keepattributes Exceptions | |
| -keepattributes RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,AnnotationDefault | |
| # Ресурсы | |
| -adaptresourcefilenames **.properties | |
| -adaptresourcefilecontents **.properties,META-INF/MANIFEST.MF | |
| ``` | |
| Данные правила обеспечивают корректную работу SDK при включённой минификации и обфускации кода. Правила не должны конфликтовать с существующими правилами сжатия в проекте банка | |
| ### 13.5 iOS | |
| 1. Перетащить `TGFISBIN.xcframework` в проект Xcode | |
| 2. В диалоге выбрать «Copy items if needed» и указать target | |
| 3. Проверить: Target → Frameworks, Libraries, and Embedded Content → «Embed & Sign» | |
| Права на камеру запрашиваются автоматически в процессе работы. | |
| ### 13.6 Flutter | |
| Добавить в `pubspec.yaml`: | |
| ```yaml | |
| dependencies: | |
| facedetect_tadi_sdk: ^0.0.1 | |
| ``` | |
| Выполнить: | |
| ```bash | |
| flutter pub get | |
| ``` | |
| --- | |
| ## 14. Кастомная вёрстка и интеграция с нативным backend | |
| ### 14.1 Обзор | |
| SDK использует WebView для отображения интерфейса верификации. При кастомной вёрстке необходимо обеспечить взаимодействие между JavaScript-кодом в WebView и нативным приложением. Взаимодействие осуществляется через JavaScript-интерфейсы (bridges). | |
| ### 14.2 Параметры запуска WebView | |
| Для корректной работы SDK необходимо передать параметры в URL: | |
| | Параметр | Тип | Описание | | |
| |----------|-----|----------| | |
| | `locale` | string | Язык интерфейса: `en`, `ru`, `uz` | | |
| | `token` | string | JWT-токен авторизации | | |
| | `native` | number | Обязательно `1` для интеграции с нативным backend | | |
| Формат URL: | |
| ``` | |
| https://gsisdk.tadi.uz/?locale=ru&token=YOUR_JWT_TOKEN&native=1 | |
| ``` | |
| Параметр `native=1` является обязательным. Без него WebView будет запрашивать доступ к камере через `getUserMedia`, как в браузере, без интеграции с нативным приложением. | |
| ### 14.3 JavaScript-интерфейсы по платформам | |
| #### iOS | |
| ```javascript | |
| // Отправка данных в нативное приложение | |
| window.webkit.messageHandlers.sendDataMessage.postMessage(person: string) | |
| // Запрос видео с камеры | |
| window.webkit.messageHandlers.requestVideoMessage.postMessage() | |
| ``` | |
| #### Android | |
| ```javascript | |
| // Отправка данных в нативное приложение | |
| window.AndroidBinding.sendDataMessage(person: string) | |
| // Запрос видео с камеры | |
| window.AndroidBinding.requestVideoMessage() | |
| ``` | |
| ### 14.4 Глобальные функции для работы с видео | |
| При кастомной вёрстке необходимо объявить следующие глобальные функции, которые будут вызываться нативным кодом: | |
| ```typescript | |
| declare global { | |
| interface Window { | |
| // Добавление чанка видео (base64) | |
| addBase64Chunk(chunk: string): void; | |
| // Завершение записи и отправка видео | |
| submitVideo(): void; | |
| // Отмена записи видео | |
| cancelVideo(): void; | |
| // iOS bridge | |
| webkit: { | |
| messageHandlers: { | |
| sendDataMessage: { | |
| postMessage: (data: string) => void; | |
| }; | |
| requestVideoMessage: { | |
| postMessage: () => void; | |
| }; | |
| }; | |
| }; | |
| // Android bridge | |
| AndroidBinding: { | |
| sendDataMessage: (data: string) => void; | |
| requestVideoMessage: () => void; | |
| }; | |
| } | |
| } | |
| ``` | |
| | Функция | Описание | | |
| |---------|----------| | |
| | `addBase64Chunk(chunk)` | Нативный код вызывает эту функцию для передачи чанков видео в формате base64 | | |
| | `submitVideo()` | Нативный код вызывает после завершения записи видео | | |
| | `cancelVideo()` | Нативный код вызывает при отмене записи видео пользователем | | |
| ### 14.5 Пример запроса видео с нативного backend | |
| ```typescript | |
| import { get } from 'lodash-es'; | |
| export const VerifyNativeView = ({ onDone }: IVerifyNativeViewProps) => { | |
| useEffect(() => { | |
| const videoChunks: string[] = []; | |
| // Функция для добавления чанка видео | |
| window.addBase64Chunk = (chunk: string) => { | |
| videoChunks.push(chunk); | |
| }; | |
| // Функция для завершения записи и отправки видео | |
| window.submitVideo = () => { | |
| onDone(); | |
| const videoBlob = base64ChunksToBlob(videoChunks); | |
| // Отправка видео на сервер верификации | |
| sendVideoForVerification(videoBlob); | |
| }; | |
| // Функция для отмены записи видео | |
| window.cancelVideo = () => { | |
| onDone(); | |
| // Обработка отмены | |
| }; | |
| // Запрос видео на iOS | |
| if (get(window, "webkit.messageHandlers.requestVideoMessage.postMessage")) { | |
| window.webkit?.messageHandlers?.requestVideoMessage?.postMessage(); | |
| } | |
| // Запрос видео на Android | |
| if (get(window, "AndroidBinding.requestVideoMessage")) { | |
| window.AndroidBinding?.requestVideoMessage(); | |
| } | |
| }, []); | |
| return <div>Записываем видео...</div>; | |
| }; | |
| // Преобразование base64 чанков в Blob | |
| function base64ChunksToBlob(chunks: string[]): Blob { | |
| const base64 = chunks.join(''); | |
| const byteCharacters = atob(base64); | |
| const byteNumbers = new Array(byteCharacters.length); | |
| for (let i = 0; i < byteCharacters.length; i++) { | |
| byteNumbers[i] = byteCharacters.charCodeAt(i); | |
| } | |
| const byteArray = new Uint8Array(byteNumbers); | |
| return new Blob([byteArray], { type: 'video/mp4' }); | |
| } | |
| ``` | |
| ### 14.6 Отправка результатов верификации в нативное приложение | |
| После успешной верификации необходимо передать данные в нативное приложение: | |
| ```typescript | |
| import { get } from 'lodash-es'; | |
| export const PersonView = ({ onDone, person }: IPersonViewProps) => { | |
| const handleSubmit = async () => { | |
| const personJson = JSON.stringify(person); | |
| // Отправка данных на iOS | |
| if (get(window, "webkit.messageHandlers.sendDataMessage.postMessage")) { | |
| window.webkit?.messageHandlers?.sendDataMessage?.postMessage(personJson); | |
| } | |
| // Отправка данных на Android | |
| if (get(window, "AndroidBinding.sendDataMessage")) { | |
| window.AndroidBinding?.sendDataMessage(personJson); | |
| } | |
| onDone(); | |
| }; | |
| return ( | |
| <div> | |
| <h2>Верификация успешна</h2> | |
| <p>ФИО: {person.last_name} {person.first_name} {person.patronym}</p> | |
| <p>ПИНФЛ: {person.pin}</p> | |
| <button onClick={handleSubmit}>Подтвердить</button> | |
| </div> | |
| ); | |
| }; | |
| ``` | |
| ### 14.7 Интерфейс данных пользователя (IPerson) | |
| При интеграции с ГЦИ используется формат snake_case: | |
| ```typescript | |
| export interface IPerson { | |
| // Статус субъекта | |
| subject_state: number; | |
| subject_state_name: string; | |
| // Идентификация | |
| pin: string; // ПИНФЛ | |
| // ФИО | |
| last_name: string; | |
| first_name: string; | |
| patronym: string; | |
| surname: string; | |
| givenname: string; | |
| // Дата рождения и пол | |
| birth_date: string; | |
| gender: number; | |
| gender_name: string; | |
| // Документ | |
| document: string; | |
| doc_seria: string; | |
| doc_number: string; | |
| date_issue: string; | |
| date_expiry: string; | |
| give_place: string; | |
| give_place_name: string; | |
| // Место рождения | |
| birth_country: string; | |
| birth_country_name: string; | |
| birth_place: string; | |
| // Национальность и гражданство | |
| nationality: string; | |
| nationality_name: string; | |
| citizenship: string; | |
| citizenship_name: string; | |
| // Результаты верификации | |
| score: number; // Коэффициент сходства (0.0 — 1.0) | |
| capture: string; // Кадр из видео (base64) | |
| photo: string; // Эталонное фото из ГЦП (base64) | |
| } | |
| ``` | |
| ### 14.8 Закрытие WebView | |
| После получения данных верификации нативное приложение должно закрыть WebView: | |
| ```kotlin | |
| // Android | |
| webView.loadUrl("about:blank") | |
| webView.destroy() | |
| ``` | |
| ```swift | |
| // iOS | |
| webView.load(URLRequest(url: URL(string: "about:blank")!)) | |
| webView.removeFromSuperview() | |
| ``` | |
| ### 14.9 Полный пример формирования URL | |
| ```typescript | |
| interface SDKParams { | |
| locale: 'en' | 'ru' | 'uz'; | |
| token: string; | |
| baseUrl: string; | |
| } | |
| function buildWebViewUrl(params: SDKParams): string { | |
| const { locale, token, baseUrl } = params; | |
| const url = new URL(baseUrl); | |
| url.searchParams.set('locale', locale); | |
| url.searchParams.set('token', token); | |
| url.searchParams.set('native', '1'); // Обязательный параметр | |
| return url.toString(); | |
| } | |
| // Использование | |
| const webViewUrl = buildWebViewUrl({ | |
| locale: 'ru', | |
| token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', | |
| baseUrl: 'https://gsisdk.tadi.uz/', | |
| }); | |
| console.log(webViewUrl); | |
| // https://gsisdk.tadi.uz/?locale=ru&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&native=1 | |
| ``` | |
| --- | |
| ## 15. UI/UX | |
| SDK предоставляет базовый UI в фирменном стиле ГЦИ. Возможна полная кастомизация через замену `index.html` и сопутствующих файлов — можно запрограммировать frontend с нуля. | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Локализация | Узбекский, русский, английский | | |
| | Темы | Светлая и тёмная | | |
| | Accessibility | Поддерживается | | |
| Технических ограничений на кастомизацию нет. Могут быть административные ограничения со стороны ГЦИ. | |
| --- | |
| ## 16. Тестирование | |
| ### 15.1 Staging-окружение | |
| Staging находится на отдельном домене с Public FQDN: `face.mbabm.uz`. Для тестирования доступны моковые физические лица с тестовыми ПИНФЛ и паспортными данными. | |
| ### 15.2 Режимы тестирования | |
| - Режим без Mutual SSL для проверки на компьютере разработчика | |
| - Staging с моковыми верификациями | |
| - Моковый сервер для `/api/v1/verify` и `/api/v1/verify_b64` | |
| ### 15.3 Rate limiting | |
| На staging подразумевается не более 1 запроса в 15 секунд на пользователя. Rate limiter должен быть установлен банком. | |
| --- | |
| ## 17. Производительность | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Время инициализации | < 800 мс | | |
| | Время верификации | ~15 секунд | | |
| | Потребление RAM (форма паспорта) | До 300 МБ | | |
| | Потребление RAM (обработка видео) | До 2 ГБ | | |
| При сворачивании приложения модальное окно остаётся открытым до завершения или отмены верификации. Освобождение ресурсов происходит автоматически. | |
| --- | |
| ## 18. Версионирование и обновления | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Обратная совместимость | Абсолютная | | |
| | Частота обновлений | ~1 релиз в месяц | | |
| | Changelog | Предоставляется | | |
| | Уведомления | Telegram-чат + email | | |
| Существующие интерфейсы API сохраняются. Новые возможности добавляются через feature model. | |
| --- | |
| ## 19. Поддержка | |
| | Параметр | Значение | | |
| |----------|----------| | |
| | Каналы | Telegram-чат, прямая связь с разработчиками, выезд в офис | | |
| | SLA на ответы | До 1 календарной недели | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment