TadSigningSDK faqat bitta metodni taqdim etadi — sign(dto). Bank ilovasining dasturchisi bitta qator yozadi:
let result = await TadSigning.sign(dto: ["amount": 1000, "to": "IBAN..."])Va javob sifatida statusOk yoki statusError(error:, errorCode:) oladi. Qolgan barchasi — SDK ichida.
Keyinchalik SDK MethodChannel / FFI orqali Flutter o'ramini oladi — bu nativ iOS/Android SDK modullariga chaqiruvlarni yo'naltiradigan ingichka abstraksiya qatlami. Dart'da SDK alohida amalga oshirilmaydi. Dart kod dekompilyatsiya qilinadi va VM ichida ishlaydi, shuning uchun barcha kriptografiya, biometriya, saqlash va tarmoq chaqiruvlari nativ tomonda qoladi.
iOS SDK'ning ommaviy xulq-atvoriga talablar:
errorCode— bu enum kabi satr, uning nomlari iOS va Android'da bir xilda tabiiy yangraydi.WEBAUTHN_USER_NOT_FOUND,INTEGRITY_TAMPERED_OS,JWT_SIGNATURE_INVALID— neytral.KEYCHAIN_FAILURE,SECURE_ENCLAVE_UNAVAILABLE,KEYSTORE_LOCKED,STRONGBOX_UNAVAILABLE,INTEGRITY_JAILBREAK(faqat iOS atamasi),INTEGRITY_ROOT(faqat Android atamasi) — platformaga bog'liq, tashqariga chiqmasligi kerak. Ichida iOS SDK istalgan tekshiruvlarni va istalgan platforma API'larini ishlatishi mumkin; ommaviyerrorCode'da bularning barchasi umumiy nomlar to'plamiga normallashtiriladi.- Barcha WEBAUTHN xatolari backend'dan keladi. SDK ularni o'ylab topmaydi — faqat satr sifatida tashqariga uzatadi va
WEBAUTHN_prefiksi bo'yicha registratsiyani wipe qilish haqida qaror qabul qiladi. - Saqlash — har bir platformaning umumiy interfeys ortidagi nativ mexanizmi. iOS'da — Keychain. Android'da — EncryptedSharedPreferences. Flutter'da hech qanday yangilik qo'shilmaydi: MethodChannel chaqiruvni platformaga uzatadi va o'sha Keychain yoki o'sha EncryptedSharedPreferences ishlaydi. Hech qanday
flutter_secure_storageyoki boshqa "kross-platforma" yechimi yo'q. Yagona talab — iOS SDK ichida Keychain'ni ingichkaSecureStorageinterfeysi ortiga olib o'tish, shunda kuzgu Android SDK'da xuddi shunday kontrakt bo'ladi. - Har bir ommaviy API metodida kuzgu Kotlin ekvivalenti bo'lishi shart, aks holda MethodChannel uni uzatolmaydi. Metod nomlari va enum kodlari yagona.
Ommaviy errorCode'ga platformaga xos nom qo'shadigan yoki iOS/Android API kuzgu mosligini buzadigan har qanday PR rad etilishi kerak.
TadSigning.configure(
apiBaseUrl: URL(string: "https://signing.tadi.uz")!,
publicKeyPem: "-----BEGIN PUBLIC KEY-----\n...",
rpId: "signing.tadi.uz",
serviceName: "tad-signing-demo",
bankId: "demo-user-001" // shu yerda qotirilgan, sign()'ga uzatilmaydi
)public enum TadSigning {
/// SDK konfiguratsiyasi. Ilova ishga tushirilganda bir marta chaqiriladi.
public static func configure(
apiBaseUrl: URL,
publicKeyPem: String,
rpId: String,
serviceName: String,
bankId: String
)
/// Registratsiya tekshiruvi. Lokal: agar himoyalangan saqlashda joriy
/// bankId uchun (deviceId, credentialId) jufti mavjud bo'lsa true qaytaradi.
/// Tarmoq so'rovlari yo'q.
public static func isRegistered() -> Bool
/// Lokal saqlashdan registratsiyani o'chiradi.
/// deviceId — bu SDK tomonidan registratsiya paytida chiqarilgan tasodifiy UUID;
/// u qurilmaga BOG'LANMAGAN va qayta ishlatilmaydi. logout()'dan keyingi
/// birinchi sign() yangi tasodifiy deviceId bilan to'liq registratsiyani bajaradi.
/// Tarmoq so'rovlari yo'q; serverdagi credential o'zi muddati tugaydi.
public static func logout()
/// isRegistered() bayrog'i bo'yicha BITTA operatsiyani bajaradi:
/// - agar isRegistered() == false — registratsiya bajariladi. Bitta /api/v1/auth/register/* chaqiruvi.
/// - agar isRegistered() == true — verifikatsiya bajariladi. Bitta /api/v1/auth/verify chaqiruvi.
/// Har bir sign() chaqiruvida aniq BITTA operatsiya, ikkalasi emas.
///
/// errorCode WEBAUTHN_, JWT_ yoki INTEGRITY_ prefiksi bilan bo'lsa — SDK lokal
/// registratsiyani tozalaydi va statusError qaytaradi. Chaqiruvchi sign(dto)'ni
/// qaytadan chaqirishi mumkin — keyingi chaqiruv registratsiyadan boshlanadi.
public static func sign(dto: [String: Any]) async -> TadSigningStatus
}
public enum TadSigningStatus {
case statusOk
case statusError(error: String, errorCode: String)
}| Maydon | Mazmuni |
|---|---|
error |
Loglar / foydalanuvchi dialogi uchun odam o'qiydigan xabar |
errorCode |
Satr. To'rt sinfdan biri — quyidagi jadvalga qarang |
JWT, deviceId, credentialId, verificationId, registrationId — chaqiruvchiga qaytarilmaydi.
| Sinf / prefiks | Manba | SDK nima qiladi |
|---|---|---|
WEBAUTHN_* (masalan, WEBAUTHN_USER_NOT_FOUND, WEBAUTHN_FACE_EXPIRED, WEBAUTHN_REPLAY_USED, WEBAUTHN_EMBEDDING_MISMATCH, WEBAUTHN_SIGN_COUNT, WEBAUTHN_REGISTRATION_ID_MISMATCH, WEBAUTHN_CHALLENGE_EXPIRED, WEBAUTHN_DTO_EXPIRED) |
Server | Registratsiyani wipe qiladi, statusError qaytaradi. Auto-retry yo'q. Keyingi sign() registratsiyadan boshlanadi. |
JWT_* (masalan, JWT_SIGNATURE_INVALID, JWT_EXPIRED, JWT_DEVICE_MISMATCH, JWT_BANK_MISMATCH, JWT_FIELD_MISMATCH, JWT_ALG_MISMATCH) |
SDK ichida public.pem va HOWTO.md bo'yicha lokal JWT validatsiya |
Registratsiyani wipe qiladi, statusError qaytaradi. Imzo buzilgan — davom ettirib bo'lmaydi. |
INTEGRITY_* (masalan, INTEGRITY_TAMPERED_OS, INTEGRITY_DEBUGGER, INTEGRITY_PROXY, INTEGRITY_HOOKING, INTEGRITY_EMULATOR, INTEGRITY_PINNING) |
Lokal yaxlitlik tekshiruvlari | Registratsiyani wipe qiladi, statusError qaytaradi. Ichida SDK istalgan tekshiruvlarni amalga oshirishi mumkin; tashqariga faqat shu neytral satrlar chiqadi. INTEGRITY_TAMPERED_OS jailbreak (iOS) va root (Android)'ni qoplaydi — ikkala platforma uchun yagona nom. |
FACEIDS_*, USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK |
Server / foydalanuvchi / platforma | wipe'siz statusError. Registratsiya yaroqli qoladi, keyingi sign() qisqa "faqat verifikatsiya" yo'lidan o'tadi. |
Ommaviy errorCode'da hech qanday platformaga xos nomlar (SECURE_ENCLAVE_*, KEYCHAIN_*, KEYSTORE_*, STRONGBOX_*, JAILBREAK_*, ROOT_*, INTEGRITY_ prefiksisiz PROXY_*) bo'lmasligi kerak. Har bir nom iOS va Android'da bir xil tabiiy o'qilishi shart — bu Flutter o'ramining talabi, u ikkala platformadan bir xil qiymatlarni proksilaydi.
flowchart TD
Start([App: TadSigning.sign(dto)])
Start --> Integrity{Integrity checks<br/>pass?}
Integrity -- no --> ErrInt([statusError<br/>INTEGRITY_xxx])
Integrity -- yes --> Lock[bankId bo'yicha<br/>mutex olish]
Lock --> Read{isRegistered?<br/>saqlashda<br/>deviceId + credentialId bormi}
Read -- no --> Reg[Subflow: registratsiya<br/>bitta /api/v1/auth/register/* chaqiruvi]
Read -- yes --> Ver[Subflow: verifikatsiya<br/>bitta /api/v1/auth/verify chaqiruvi]
Reg -- ok --> Done([statusOk<br/>registratsiya yozildi])
Reg -- error --> WipeIfDirty[Agar yozuv qisman<br/>yaratilgan bo'lsa — wipe]
WipeIfDirty --> ErrReg([statusError<br/>aniq kod])
Ver -- ok --> Done
Ver -- WEBAUTHN_xxx --> Wipe[saqlashdan<br/>registratsiyani wipe qilish]
Ver -- JWT_xxx --> Wipe
Ver -- INTEGRITY_xxx --> Wipe
Ver -- other error --> ErrOther([statusError<br/>aniq kod<br/>registratsiya butun])
Wipe --> ErrWipe([statusError<br/>aniq kod<br/>keyingi sign()<br/>registratsiyadan boshlanadi])
Done --> Unlock[mutex'ni qaytarish]
ErrInt --> Unlock
ErrReg --> Unlock
ErrOther --> Unlock
ErrWipe --> Unlock
classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
classDef sub fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
classDef ok fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313,font-weight:bold
classDef rec fill:#FFE8CC,stroke:#A86200,color:#5C3700
class Integrity,Read chk
class Reg,Ver,Lock,Unlock,WipeIfDirty sub
class Done ok
class ErrInt,ErrReg,ErrOther,ErrWipe err
class Wipe rec
Asosiy qoidalar:
sign()har bir chaqiruvda bitta operatsiya bajaradi: yo registratsiya, yo verifikatsiya. Ikkalasi emas. QarorisRegistered()bo'yicha qabul qilinadi.- Agar
isRegistered() == false—sign()registratsiyani bajaradi. Muvaffaqiyatdan keyin saqlashdadeviceId + credentialIdjufti paydo bo'ladi vasign()statusOkqaytaradi. Bu chaqiruvda DTO imzolanmagan — imzolash uchun yana bittasign()chaqiruvi kerak. - Agar
isRegistered() == true—sign()uzatilgandtobilan verifikatsiyani bajaradi. Muvaffaqiyatdan keyinstatusOk, registratsiya saqlanadi. - Agar
verifyWEBAUTHN_*/JWT_*/INTEGRITY_*bilan tushsa — wipe +statusError. Keyingisign()registratsiyadan boshlanadi (chunkiisRegistered()endifalse). - Boshqa barcha xatolar (
USER_CANCELLED,CAMERA_UNAVAILABLE,NETWORK,FACEIDS_*) — wipe'sizstatusError. Registratsiya yaroqli qoladi. - Bitta
sign()chaqiruvi ichida auto-retry yo'q.
Hujjatni imzolash uchun chaqiruvchi koddagi tipik ketma-ketlik:
if !TadSigning.isRegistered() { // birinchi sign() registratsiyani bajaradi; bo'sh dto bo'lishi mumkin let s1 = await TadSigning.sign(dto: [:]) guard case .statusOk = s1 else { return /* xatoni qayta ishlash */ } } // registratsiyadan keyin — haqiqiy DTO bilan sign() imzolashni bajaradi let s2 = await TadSigning.sign(dto: realDto)Agar registratsiya allaqachon mavjud bo'lsa, birinchi
ifo'tkazib yuboriladi va imzolash bitta chaqiruvda amalga oshiriladi.
flowchart TD
Start([Subflow: registratsiya])
Start --> Gen[deviceId = UUID()<br/>HAR registratsiya uchun tasodifiy,<br/>qurilmaga bog'lanmagan]
Gen --> Cam[Kamera + liveness, 2 kadr]
Cam -- error --> ErrCam([fail: USER_CANCELLED /<br/>CAMERA_UNAVAILABLE])
Cam -- ok --> Cap[POST /api/v1/check/capture]
Cap --> CapV{public.pem bo'yicha<br/>JWT verifikatsiyasi<br/>HOWTO.md}
CapV -- error --> ErrJwt([fail: JWT_xxx])
CapV -- ok --> Opts[POST /api/v1/auth/register/options]
Opts --> OptsV{JWT verifikatsiyasi<br/>+ decoded.dto deepEqual request.dto<br/>HOWTO.md}
OptsV -- error --> ErrJwt
OptsV -- ok --> SE[Passkey:<br/>kalit yaratish,<br/>challenge'ni imzolash]
SE -- error --> ErrLocal([fail: lokal kod<br/>masalan, USER_CANCELLED])
SE -- ok --> Ver[POST /api/v1/auth/register/verify]
Ver --> VerV{JWT verifikatsiyasi<br/>+ credentialId === data.credentialId<br/>HOWTO.md}
VerV -- error --> ErrJwt
VerV -- ok --> Save[ATOMAR saqlash:<br/>deviceId + credentialId]
Save --> Done([ok])
classDef sdk fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
classDef api fill:#F4F4F2,stroke:#666666,color:#1A1A1A
classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
classDef se fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ok fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313
class Gen,Cam,Save sdk
class Cap,Opts,Ver api
class CapV,OptsV,VerV chk
class SE se
class Done ok
class ErrCam,ErrJwt,ErrLocal err
flowchart TD
Start([Subflow: verifikatsiya<br/>identity = deviceId + credentialId<br/>lokal saqlashdan])
Start --> Cam[Kamera + liveness, 2 kadr]
Cam -- error --> ErrCam([fail: USER_CANCELLED /<br/>CAMERA_UNAVAILABLE])
Cam -- ok --> Cap[POST /api/v1/check/capture]
Cap --> CapV{JWT verifikatsiyasi<br/>HOWTO.md}
CapV -- error --> ErrJwt([fail: JWT_xxx<br/>chaqiruvchi wipe qiladi])
CapV -- ok --> Opts[POST /api/v1/auth/verify/options]
Opts --> WErr1{Server<br/>WEBAUTHN_xxx qaytardimi?}
WErr1 -- yes --> WebAuth([fail: WEBAUTHN_xxx<br/>chaqiruvchi wipe qiladi])
WErr1 -- no --> OptsV{JWT verifikatsiyasi<br/>+ decoded.dto deepEqual request.dto<br/>HOWTO.md}
OptsV -- error --> ErrJwt
OptsV -- ok --> SE[Passkey:<br/>credentialId orqali<br/>challengeJwt'ni imzolash]
SE -- error --> ErrLocal([fail: lokal kod])
SE -- ok --> Ver[POST /api/v1/auth/verify]
Ver --> WErr2{Server<br/>WEBAUTHN_xxx qaytardimi?}
WErr2 -- yes --> WebAuth
WErr2 -- no --> Other{Server FACEIDS_xxx<br/>yoki boshqa xato qaytardimi?}
Other -- yes --> ErrOther([fail: FACEIDS_xxx<br/>registratsiya butun])
Other -- no --> VerV{JWT verifikatsiyasi<br/>+ dto maydonlari deepEqual data<br/>HOWTO.md}
VerV -- error --> ErrJwt
VerV -- ok --> Done([ok<br/>jwt SDK ichida qoladi])
classDef sdk fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
classDef api fill:#F4F4F2,stroke:#666666,color:#1A1A1A
classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
classDef se fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ok fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313
classDef wb fill:#FFE8CC,stroke:#A86200,color:#5C3700
class Cam sdk
class Cap,Opts,Ver api
class CapV,OptsV,VerV,WErr1,WErr2,Other chk
class SE se
class Done ok
class ErrCam,ErrJwt,ErrLocal,ErrOther err
class WebAuth wb
sequenceDiagram
autonumber
participant App as Bank ilova kodi
participant SDK as TadSigningSDK
participant Store as Saqlash
participant API as Server
App->>SDK: TadSigning.sign(dto)
SDK->>SDK: integrity checks
SDK->>Store: read(bankId)
alt juft yo'q (isRegistered == false) — REGISTRATSIYA
Store-->>SDK: nil
Note over SDK: yangi deviceId bilan<br/>bitta /api/v1/auth/register/* chaqiruvi
SDK->>API: POST /api/v1/check/capture<br/>POST /api/v1/auth/register/options<br/>POST /api/v1/auth/register/verify
alt register ok
API-->>SDK: credentialId
SDK->>Store: save(deviceId, credentialId)
SDK-->>App: statusOk
Note over App: bu chaqiruvda dto IMZOLANMAGAN —<br/>imzolash uchun sign(dto)'ni qayta chaqiring
else register error
API-->>SDK: error
SDK->>Store: wipe (agar yozuv qisman yaratilgan bo'lsa)
SDK-->>App: statusError(errorCode: ...)
end
else juft mavjud (isRegistered == true) — VERIFIKATSIYA
Store-->>SDK: deviceId, credentialId
Note over SDK: uzatilgan dto bilan<br/>bitta /api/v1/auth/verify chaqiruvi
SDK->>API: POST /api/v1/check/capture<br/>POST /api/v1/auth/verify/options<br/>POST /api/v1/auth/verify
alt API: WEBAUTHN_xxx
API-->>SDK: error WEBAUTHN_xxx
SDK->>Store: wipe(bankId)
SDK-->>App: statusError(errorCode: WEBAUTHN_xxx)
Note over App: keyingi sign() registratsiyadan boshlanadi
else local: JWT_xxx
Note over SDK: public.pem bo'yicha JWT verifikatsiyasi xatosi
SDK->>Store: wipe(bankId)
SDK-->>App: statusError(errorCode: JWT_xxx)
else local: INTEGRITY_xxx
Note over SDK: jailbreak / Frida / proxy / ...
SDK->>Store: wipe(bankId)
SDK-->>App: statusError(errorCode: INTEGRITY_xxx)
else API: FACEIDS_xxx
API-->>SDK: error FACEIDS_xxx
Note over SDK,Store: registratsiya butun — wipe yo'q
SDK-->>App: statusError(errorCode: FACEIDS_xxx)
else local: USER_CANCELLED / CAMERA_UNAVAILABLE / NETWORK
Note over SDK,Store: registratsiya butun — wipe yo'q
SDK-->>App: statusError(errorCode: ...)
else verify ok
API-->>SDK: signature
SDK-->>App: statusOk
end
end
funksiya sign(dto):
// 1. Muhit yaxlitligi (jailbreak / Frida / proxy / pinning / ...)
agar !checkIntegrity():
secureStorage.wipe(bankId) // registratsiya xavf ostida
qaytarish statusError(error, errorCode: "INTEGRITY_xxx")
mutex(bankId) ostida:
identity = secureStorage.read(bankId)
// BRANCH A: registratsiya yo'q → FAQAT registratsiya bajariladi
agar identity == nil:
natija = doRegister(yangi tasodifiy deviceId)
agar natija.ok:
secureStorage.save(bankId, natija.identity)
qaytarish statusOk
aks holda:
// agar yozuv qisman yaratilgan bo'lsa — wipe; UUID bir martalik
agar secureStorage.read(bankId) != nil:
secureStorage.wipe(bankId)
qaytarish statusError(error, errorCode: natija.errorCode)
// BRANCH B: registratsiya mavjud → FAQAT verifikatsiya bajariladi
natija = doVerify(identity, dto)
agar natija.ok:
qaytarish statusOk
kod = natija.errorCode
// registratsiya buzilgan deb hisoblanadigan xato sinflari
agar kod.hasPrefix("WEBAUTHN_")
yoki kod.hasPrefix("JWT_")
yoki kod.hasPrefix("INTEGRITY_"):
secureStorage.wipe(bankId)
qaytarish statusError(error, errorCode: kod)
// FACEIDS_*, USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK — registratsiya butun
qaytarish statusError(error, errorCode: kod)
funksiya isRegistered() -> Bool:
qaytarish secureStorage.read(bankId) != nil
funksiya logout():
secureStorage.wipe(bankId)
// deviceId — SDK tomonidan registratsiya paytida chiqarilgan tasodifiy UUID edi.
// U bir martalik va qurilmaga bog'lanmagan. logout()'dan keyin
// har qanday keyingi registratsiya YANGI tasodifiy UUID chiqaradi.
Wipe qoidalari:
errorCode.hasPrefix("WEBAUTHN_")— server tomoni bizning juftni endi tan olmaydi → wipe.errorCode.hasPrefix("JWT_")—public.pembo'yicha lokal JWT validatsiyasi (qarangHOWTO.md) o'tmadi → wipe (sessiya buzilgan).errorCode.hasPrefix("INTEGRITY_")— ishga tushirish muhiti buzilgan → wipe.- Boshqa barchasi (
FACEIDS_*,USER_CANCELLED,CAMERA_UNAVAILABLE,NETWORK) — registratsiya yaroqli qoladi.
sign() ichida auto-retry yo'q. Chaqiruvchi isRegistered()'ni tekshirishi va/yoki sign()'ni shunchaki qayta chaqirishi mumkin — wipe'dan keyingi chaqiruv yangi tasodifiy deviceId bilan yangi registratsiyadan boshlanadi.
deviceId — bu SDK tomonidan har bir yangi registratsiya uchun generatsiya qilinadigan tasodifiy UUID. Bu qurilma identifikatori emas, identifierForVendor emas, hech qanday turg'un kod emas. Xususiyatlari:
logout()'dan keyin yokiWEBAUTHN_*/JWT_*/INTEGRITY_*xato tufayli wipe'dan keyingi har bir birinchisign()'da qaytadan chiqariladi.- Qurilmaning apparat xususiyatlaridan, IMEI, seriya raqamlaridan va h.k.dan kelib chiqmaydi — faqat kriptografik mustahkam generator orqali
UUID(). - Bitta jismoniy qurilmada uning butun hayoti davomida o'nlab turli
deviceId'lar mavjud bo'lishi mumkin — bu normal. - Bitta
deviceIdo'z registratsiyasidan o'z wipe yokilogout()'gacha yashaydi.
Nega shunday: server tomoni (MongoDB) {deviceId, bankId} unikal kompozit indeksini ushlab turadi va credential yaratishdan oldin uning mavjudligini aniq tekshiradi. Eski deviceId bilan qayta ro'yxatdan o'tishga har qanday urinish rad etiladi. Shuning uchun yagona to'g'ri yo'l — har bir registratsiyada doim yangi tasodifiy UUID chiqarish.
Chaqiruvchi kod bu bilimni ishlatmaydi va deviceId'ni tashqariga olmaydi. Ammo SDK jamoasi buni tushunishi shart, "keling, deviceId'ni keshlaymiz va qayta ishlatamiz" turidagi xatolarga yo'l qo'ymaslik uchun.
Saqlash kontrakti abstrakt — bu Flutter ulashi uchun talab. iOS uchun aniq amalga oshirish SecureStorage interfeysi (read / save / wipe) ortida yashiringan va SDK'ning ommaviy kontraktiga ta'sir qilmaydi.
| Parametr | Talab |
|---|---|
| Kontrakt | read(bankId) -> Identity?, save(bankId, Identity), wipe(bankId), wipeAll() |
| Kafolat | Yozuv boshqa ilovalarga ko'rinmaydi, bulutli backup'ga chiqmaydi, ilova o'chirilganda saqlanmaydi |
| Yozuv formati (JSON) | { "deviceId": "<uuid>", "credentialId": "<base64url>", "registeredAt": "<iso8601>" } |
| Yozuv identifikatsiyasi | TadSigningConfig'dan bankId bo'yicha |
| Parallellik | bankId bo'yicha mutex / actor. Parallel sign()'lar serializatsiya qilinadi. |
| Loglar | deviceId, credentialId maskalanadi (4+4 belgi) |
iOS'da amalga oshirish (ichki tafsilot, ommaviy API qismi emas): Keychain, kSecClassGenericPassword, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrSynchronizable = false. Kuzgu Android SDK'da — EncryptedSharedPreferences (AES-256-GCM, AndroidKeyStore). MethodChannel orqali Flutter o'rami iOS'da o'sha Keychain'ni va Android'da o'sha EncryptedSharedPreferences'ni qayta ishlatadi — Dart tomonida alohida amalga oshirish ko'zda tutilmagan.
Hech qanday UserDefaults, Documents'dagi fayllar, takroriy nusxalar yo'q.
| Endpoint | So'rov | Javob data |
|---|---|---|
POST /api/v1/check/capture |
deviceId, bankId, originalImageBase64 |
hash |
POST /api/v1/auth/register/options |
deviceId, bankId, embeddingChecksum, dto |
registrationId, challenge, ...WebAuthnOptions |
POST /api/v1/auth/register/verify |
registrationId, attestation, originalImageBase64 |
credentialId |
POST /api/v1/auth/verify/options |
deviceId, bankId, dto |
verificationId, challengeJwt |
POST /api/v1/auth/verify |
payload {deviceId, bankId, verificationId, replay}, assertion, originalImageBase64, compareImageBase64 |
dto |
Har bir muvaffaqiyatli javob signature maydonini o'z ichiga oladi — server'ning maxfiy kaliti bilan imzolangan JWT. Algoritm TadSigningConfig.publicKeyPem'dan o'qiladi (joriy — ES512). TTL — 15 daqiqa.
| Endpoint | deviceId, bankId, exp'dan tashqari |
|---|---|
POST /api/v1/check/capture |
decoded.hash === data.hash |
POST /api/v1/auth/register/options |
decoded.registrationId === data.registrationId, decoded.challenge === data.challenge, decoded.dto deepEqual request.dto |
POST /api/v1/auth/register/verify |
decoded.credentialId === data.credentialId |
POST /api/v1/auth/verify/options |
decoded.verificationId === data.verificationId, decoded.challengeJwt === data.challengeJwt, decoded.dto deepEqual request.dto |
POST /api/v1/auth/verify |
data'da mavjud bo'lgan har bir dto maydoni decoded'dagi xuddi shu nomli maydonga mos kelishi shart |
JWT sarlavhasidagi algoritm publicKeyPem'dagi kalit algoritmiga mos kelishi shart. Har qanday nomuvofiqlik — errorCode: "JWT_ALG_MISMATCH". Hech qanday alg=none, ochiq kalit bilan hech qanday alg=HS256.
| Stsenariy | Kutilgan |
|---|---|
isRegistered() bo'sh saqlashda |
false, tarmoq so'rovlari yo'q |
Muvaffaqiyatli registratsiya sign()'idan keyin isRegistered() |
true, tarmoq so'rovlari yo'q |
Registratsiyadan keyin logout() |
isRegistered() false bo'ladi, tarmoq so'rovlari yo'q, eski deviceId hech qayerda saqlanmagan |
sign() bo'sh saqlashda |
bitta POST /api/v1/check/capture + POST /api/v1/auth/register/options + POST /api/v1/auth/register/verify chaqiruvi (POST /api/v1/auth/verify/options va POST /api/v1/auth/verify SIZ), saqlashda aniq bitta yozuv, statusOk |
sign() to'ldirilgan saqlashda |
bitta POST /api/v1/check/capture + POST /api/v1/auth/verify/options + POST /api/v1/auth/verify chaqiruvi (POST /api/v1/auth/register/options va POST /api/v1/auth/register/verify SIZ), statusOk |
sign() (registratsiya) → sign() (verifikatsiya) |
birinchisi faqat register, ikkinchisi faqat verify, ikkalasi statusOk. POST /api/v1/auth/register/options va POST /api/v1/auth/register/verify aniq birinchi chaqiruv vaqtida, POST /api/v1/auth/verify/options va POST /api/v1/auth/verify aniq ikkinchi chaqiruv vaqtida ishlatiladi. |
logout()'dan keyin sign() |
bitta register chaqiruvi, statusOk. deviceId logout()'gacha ishlatilganidan farq qiladi. |
verify → WEBAUTHN_USER_NOT_FOUND |
wipe, statusError(errorCode: "WEBAUTHN_USER_NOT_FOUND"), isRegistered() false bo'ladi. Auto-register ishga tushmaydi. |
verify → WEBAUTHN_FACE_EXPIRED |
wipe, statusError(errorCode: "WEBAUTHN_FACE_EXPIRED"), isRegistered() false bo'ladi |
verify → istalgan WEBAUTHN_* |
wipe, statusError(errorCode: <o'sha>), isRegistered() false bo'ladi |
WEBAUTHN_* tufayli wipe'dan keyin takroriy sign() |
yangi tasodifiy deviceId (wipe qilingandan farqli) bilan bitta register chaqiruvi, statusOk. Imzolash uchun yana bitta sign() kerak. |
verify JWT tekshiruvdan o'tmadi (signature, exp, deviceId mismatch) |
wipe, statusError(errorCode: "JWT_xxx"), isRegistered() false bo'ladi |
verify → FACEIDS_SIMILARITY_SCORE_TOO_LOW |
wipe'siz, statusError(errorCode: "FACEIDS_SIMILARITY_SCORE_TOO_LOW"), isRegistered() true bo'lib qoladi |
| Biometriyadan voz kechish | wipe'siz, statusError(errorCode: "USER_CANCELLED"), isRegistered() o'zgarmaydi |
| Kamera mavjud emas | wipe'siz, statusError(errorCode: "CAMERA_UNAVAILABLE") |
| Tarmoq nosozligi | wipe'siz, statusError(errorCode: "NETWORK") |
POST /api/v1/auth/register/options'da decoded.dto != request.dto |
wipe, statusError(errorCode: "JWT_FIELD_MISMATCH") |
JWT'dagi alg konfiguratsiya kalitiga mos kelmadi |
wipe, statusError(errorCode: "JWT_ALG_MISMATCH") |
| Integrity tekshiruvi ishga tushdi | wipe, statusError(errorCode: "INTEGRITY_xxx"), hech qanday tarmoq so'rovi serverga bormaydi |
Parallel sign(dto) |
mutex bilan serializatsiya qilinadi. Agar ikkalasi bo'sh saqlashda chaqirilgan bo'lsa — birinchisi register'ni bajaradi va statusOk qaytaradi, ikkinchisi yaroqli registratsiyani ko'radi va verify'ni bajaradi. |
- "Registratsiya va imzolash" stsenariysi:
logout()→sign(dto: [:])→sign(dto: realDto). Birinchisign()faqat register, ikkinchisi faqat verify bajaradi. IkkalasistatusOkqaytaradi. Ular orasida saqlashdadeviceId + credentialIdjufti paydo bo'ladi. - "Server tomonidan credential'ni o'chirish" stsenariysi: muvaffaqiyatli registratsiyadan keyin operator serverdagi credential'ni o'chiradi →
sign(dto: realDto)statusError(errorCode: "WEBAUTHN_USER_NOT_FOUND")qaytaradi va wipe qiladi → keyingisign()registratsiyani qaytadan bajaradi (endi boshqadeviceIdbilan) va DTO imzolanmasdanstatusOkqaytaradi → yana bittasign(dto: realDto)imzolaydi. - Hujum qiluvchi middleware
POST /api/v1/auth/register/verifyjavobidacredentialId'ni almashtiradi →statusError(errorCode: "JWT_FIELD_MISMATCH"), saqlashga hech narsa yozilmagan (atomarlik). - Hujum qiluvchi middleware
POST /api/v1/auth/verify/optionsjavobidadto'ni almashtiradi →statusError(errorCode: "JWT_FIELD_MISMATCH").JWT_prefiksi — wipe. Keyingisign()registratsiyadan o'tadi. - Jailbreak (iOS) yoki root (Android) bilan qurilmada
sign()tarmoq so'rovlarisizstatusError(errorCode: "INTEGRITY_TAMPERED_OS")qaytaradi.
Breaking change. Bu orqaga moslikssiz reliz. Eski API (
TadSigningViewController(mode:),TadSigningMode, completion-asoslanganTadSigningResult) to'liq o'chiriladi. Hech qanday@available(*, deprecated), hech qanday o'tish davri versiyalari, hech qanday o'ramlar yo'q. SDK iste'molchilari bir marta migratsiya qiladi — yangiTadSigning.sign(dto:)'ga.
| Modul | Nima qilinadi |
|---|---|
| Ommaviy API | TadSigning namespace: configure(), sign(), isRegistered(), logout(), TadSigningStatus. Eski TadSigningViewController(mode:), TadSigningMode va bog'liq turlar bandldan o'chiriladi. |
sign() orkestratsiyasi |
isRegistered() bo'yicha register / verify shoxlari, holat mashinasi, mutex, har registratsiya uchun tasodifiy deviceId generatori |
SecureStorage abstraksiyasi |
read / save / wipe / wipeAll interfeysi, bankId bo'yicha izolyatsiya. iOS'da Keychain amalga oshirilishi yashirin tafsilot. |
| JWT validatsiyasi | HOWTO.md bo'yicha barcha 5 endpoint uchun public.pem'ga qarshi validator: imzo, exp, deviceId / bankId, o'ziga xos maydonlar, dto deepEqual, alg-confusion guard. Barcha xatolar JWT_* prefiksi ostida. |
| Xato mapping'i | WEBAUTHN_* / JWT_* / INTEGRITY_* / boshqa prefikslar bo'yicha tasniflash → wipe yoki yo'q |
| Integrity tekshiruvlari | jailbreak / debugger / Frida / emulator / proxy / cert pinning / bandl yaxlitligi. Natija — neytral INTEGRITY_* prefiksi ostidagi errorCode. Amalga oshirish iOS'ga xos, tashqariga ko'rinmaydi. |
| Testlar | yuqoridagi jadvallar bo'yicha unit + integratsiya |
| Sample app + hujjatlar | bitta Sign tugmasi, README, migration guide |
- Server o'zgartirilmaydi. API kontrakti qotirilgan.
- Imzo algoritmi o'zgartirilmaydi (RS256 → ES512 — alohida backlog).
- Kamera / liveness UI o'zgartirilmaydi.
- JWT,
deviceId,credentialId,verificationIdommaviy imzoga chiqarilmaydi. Hech qachon. Completion orqali ham, delegate orqali ham emas.