Aller au contenu

PD-262 — iOS Anti-tampering : detection jailbreak et protection Frida

1. Objectif

Contractualiser un module anti-tampering iOS qui detecte les environnements compromis, force un lockout fail-closed, purge les secrets locaux, et emet un audit backend en best effort, sans dependance a la couche JavaScript pour les decisions critiques.

Le resultat attendu est binaire et testable : - soit tampered=false et l'application continue en mode normal ; - soit tampered=true et l'application est verrouillee de maniere non contournable dans la session, avec persistance locale de lockout.

2. Perimetre / Hors perimetre

Inclus

  • Detection de compromission iOS par controles multi-sources :
  • jailbreak (paths, sandbox, symlinks, fork, dylib loaded images),
  • instrumentation dynamique (Frida/Objection),
  • integrite de signature (bundle/provisioning coherence).
  • Execution des controles dans les contextes suivants :
  • cold start,
  • retour foreground,
  • verification periodique en session.
  • Decision locale immediate en fail-closed.
  • Purge immediate des secrets/caches sensibles (memoire + artefacts de travail locaux).
  • Ecran lockout non dismissable.
  • Emission d'evenement audit backend en best effort, sans bloquer la securite locale.
  • Activation conditionnelle par environnement :
  • inactif en DEBUG et simulateur,
  • actif en Release/TestFlight/production,
  • activable en QA sur device reel via build flag compile (ANTI_TAMPERING_QA_ENABLED) uniquement ; non remote-config et non runtime mutable.

Exclu

  • Root detection Android.
  • Wipe complet automatique des donnees persistantes chiffrees.
  • Remote unlock / deblocage distant.
  • Detection proxy SSL/MITM.
  • Obfuscation Swift.
  • Toute logique de contournement UX apres lockout.
  • Toute regle non testable est hors perimetre.

3. Definitions

3.1 Glossaire

  • Anti-tampering : ensemble de controles detectant une tentative de modification/instrumentation de l'app.
  • Fail-closed : en cas d'incertitude ou de detection, l'acces est refuse.
  • Lockout : etat de blocage fonctionnel de l'application.
  • Best effort : tentative sans garantie de succes reseau.
  • Session : periode entre ouverture app et terminaison process/reinstallation.
  • Operations sensibles : tout acces au Keychain, tout dechiffrement de donnee, tout appel API authentifie.
  • Caches crypto : artefacts temporaires purgeables (clefs en memoire, fragments, DEK temporaires, buffers de dechiffrement, previews, fichiers temporaires dechiffres).
  • Donnees persistantes chiffrees : blobs applicatifs deja chiffres au repos (base locale chiffree, fichiers chiffres metier) conserves lors du lockout ; ils restent inexploitables sans secrets purges.
  • Premier lancement clean : etat initial detecte uniquement si le marqueur first_launch_clean=true est present apres initialisation d'usine.

3.2 Modele d'etat contractuel

  • MONITORED : etat nominal, controles actifs.
  • TAMPERED_SESSION : compromission detectee, purge et verrouillage enclenches.
  • LOCKED_PERSISTENT : etat terminal local ; app inutilisable jusqu'a reinstallation propre.

3.3 Modele de donnees et formats (source unique)

Donnee Format / encodage Taille / longueur Charset / casse Regex / validation Si invalide
tampered booleen JSON 1 bit logique n/a true/false fail-closed (true)
reason_code enum string UPPER_SNAKE_CASE 3..64 chars [A-Z0-9_], case-sensitive ^(JAILBREAK_DETECTED\|FRIDA_DETECTED\|CODESIGN_INVALID\|DYLIB_INJECTION\|SANDBOX_VIOLATION\|DETECTOR_ERROR)$ rejet evenement + lockout maintenu
event_type enum string fixe ASCII upper ^ANTI_TAMPERING_LOCKOUT$ rejet evenement
app_version SemVer string 5..32 chars [0-9A-Za-z.+-], case-sensitive ^[0-9]+\.[0-9]+\.[0-9]+([\-+][0-9A-Za-z.\-]+)?$ rejet evenement
build_number entier decimal en string 1..10 chars [0-9] ^[0-9]{1,10}$ rejet evenement
device_id_pseudo hash hex lowercase de SHA-256(UIDevice.current.identifierForVendor.uuidString UTF-8) 64 chars (32 bytes) [a-f0-9], case-sensitive ^[a-f0-9]{64}$ rejet evenement
timestamp ISO-8601 UTC 20..30 chars ASCII, case-sensitive ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$ rejet evenement
environment enum string 2..16 chars [A-Z_], case-sensitive ^(DEBUG\|SIMULATOR\|QA\|RELEASE\|TESTFLIGHT\|PRODUCTION)$ rejet evenement
detection_interval_sec entier min 30 / max 60 / defaut 45 unite: secondes 30<=x<=60 clamp aux bornes
lockout_persistent_flag booleen Keychain 1 bit logique n/a valeur strictement booleenne fail-closed (LOCKED_PERSISTENT)
first_launch_clean booleen persistant d'initialisation 1 bit logique n/a valeur strictement booleenne si absent et app deja initialisee => fail-closed (LOCKED_PERSISTENT)

Note de reference : toutes les sections suivantes reutilisent ces definitions sans les redefinir.

3.4 Parametres numeriques contractuels

Parametre Defaut Min Max Unite Contexte Percentile Hors bornes
Intervalle controle periodique 45 30 60 secondes session active iOS n/a clamp
Ports Frida scannes 27042, 27043 1 65535 port TCP loopback local n/a ignorer port invalide
Delai max decision lockout apres detection 1 0 2 secondes device reel iOS release P95 fail-closed si depassement
Delai max demarrage controle cold start (budget) 1500 0 3000 ms iPhone 12, iOS 16+ P95 fail-closed si depassement
Delai max purge initiale 2 0 5 secondes device reel iOS release P95 lockout immediat puis purge retry locale
Retry purge locale 3 0 3 tentatives post-detection n/a abandon apres 3 echecs, lockout maintenu, purge incomplete loggee
Intervalle retry purge 1 1 1 seconde post-detection n/a valeur fixe contractuelle

3.5 SLA temporels

Aucune transition de type TTL/expiration/deadline metier n'est identifiee.

3.6 Persistance lockout contractuelle

  • Le lockout persistant est stocke exclusivement en Keychain (pas UserDefaults, pas remote config, pas fichier applicatif) avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.
  • Le marqueur lockout_persistent_flag=true est traite comme non neutralisable par l'utilisateur dans le cycle de vie normal de l'app ; sa suppression n'est possible qu'apres reinstallation propre (nouvelle initialisation).
  • Au boot, si lockout_persistent_flag est absent et first_launch_clean est absent alors que l'app est deja initialisee, l'etat est interprete en fail-closed LOCKED_PERSISTENT.
  • Au boot, toute corruption de la donnee lockout ou etat transitoire incomplet est interpretee en LOCKED_PERSISTENT.
  • Si le process est tue/crashe entre TAMPERED_SESSION et l'ecriture finale LOCKED_PERSISTENT, l'etat reconstruit au redemarrage est force a LOCKED_PERSISTENT.

4. Invariants (non negociables)

ID Regle Justification
INV-262-01-fail-closed Toute detection positive OU erreur de detecteur (DETECTOR_ERROR) force tampered=true. Eviter bypass par panne/contournement.
INV-262-02-native-authority La decision de lockout/purge est prise uniquement par la couche native iOS ; JS ne peut ni l'inhiber ni la retarder. Menace PD-174 S-01/S-02 (hook JS).
INV-262-03-trigger-coverage Les controles sont executes au cold start, a chaque retour foreground, et periodiquement en session. Couvrir attache Frida apres demarrage.
INV-262-04-single-lockout-state Un seul etat fonctionnel de compromission (tampered=true) ; causes distinctes uniquement en audit via reason_code. Simplicite metier + telemetrie.
INV-262-05-purge-on-detect Sur detection, purge immediate des secrets memoire, tokens session/refresh, caches crypto, temporaires dechiffres, previews. Reduction surface d'exfiltration.
INV-262-06-local-first Lockout/purge ne dependent jamais du reseau ; le reporting backend est best effort non bloquant. Disponibilite securite hors ligne.
INV-262-07-release-gating Controle inactif en DEBUG+simulateur ; actif en RELEASE/TESTFLIGHT/PRODUCTION ; activable QA sur device reel par build flag compile non mutable a runtime. Non blocage dev/test, couverture prod, anti-bypass runtime.
INV-262-08-transition-model MONITORED->TAMPERED_SESSION autorisee ; TAMPERED_SESSION->LOCKED_PERSISTENT autorisee ; retour vers etat nominal interdit. Irreversibilite de compromis session.
INV-262-09-terminal-state LOCKED_PERSISTENT -> * : INTERDITE (etat terminal, resolution manuelle uniquement) Eviter deblocage local non autorise.
INV-262-10-envelope-encryption Tout artefact cryptographique temporaire (cle, fragment, DEK, ReKey) est chiffre au repos (AES-256-GCM ou enveloppe HSM), aucun secret en clair en stockage local. Invariant crypto obligatoire.
INV-262-11-no-sensitive-telemetry Aucun dump memoire ni materiau sensible n'est emissible dans la telemetrie. Conformite securite et minimisation des donnees.
INV-262-12-state-persistence Le lockout est persiste localement et re-applique au redemarrage tant que l'app n'est pas re-installee proprement. Cohesion avec objectif de verrouillage durable.

5. Flux nominaux

Flux F1 — Cold start nominal (non compromis)

  1. App entre en MONITORED.
  2. Controles cold start executes.
  3. Aucun signal de compromission.
  4. tampered=false, deblocage fonctionnel autorise.

Flux F2 — Retour foreground nominal

  1. Transition background -> foreground.
  2. Re-check anti-tampering execute avant reprise sensible.
  3. Aucun signal de compromission.
  4. Session continue sans lockout.

Flux F3 — Detection de compromission en session

  1. Controle periodique/foreground/cold start detecte une cause.
  2. Etat devient TAMPERED_SESSION.
  3. Purge immediate lancee.
  4. Ecran lockout non dismissable affiche.
  5. Etat devient LOCKED_PERSISTENT.
  6. Evenement ANTI_TAMPERING_LOCKOUT tente en best effort.
  7. En cas de purge partielle, retries locaux bornes: max 3 tentatives a 1s d'intervalle ; abandon apres 3 echecs avec lockout maintenu et purge incomplete loggee.

Flux F4 — Reporting backend best effort

  1. Payload valide (selon §3.3) construit.
  2. Tentative d'emission backend.
  3. Si succes : audit distant complete.
  4. Si echec : lockout reste actif, absence de rollback securite.

Flux F5 — Redemarrage apres lockout

  1. App redemarree apres lockout precedent.
  2. Lecture etat persiste LOCKED_PERSISTENT en Keychain.
  3. App affiche lockout immediatement.
  4. Aucune transition sortante autorisee (etat terminal).

Flux F6 — Reconstruction d'etat au boot (cas degrade)

  1. App demarre et reconstruit l'etat anti-tampering local.
  2. Si donnee lockout corrompue, absente (hors premier lancement clean) ou transitoire/incomplete, interpretation fail-closed.
  3. Etat force a LOCKED_PERSISTENT.
  4. Lockout immediat et aucune reprise nominale.

Machine a etats (checklist transitions)

  • MONITORED : sorties
  • -> TAMPERED_SESSION : AUTORISEE (detection positive ou erreur detecteur)
  • -> LOCKED_PERSISTENT : INTERDITE (doit passer par purge/lockout sequence)
  • TAMPERED_SESSION : sorties
  • -> LOCKED_PERSISTENT : AUTORISEE
  • -> MONITORED : INTERDITE (pas de downgrade)
  • LOCKED_PERSISTENT (terminal) : sorties
  • -> * : INTERDITE (etat terminal, resolution manuelle uniquement)
  • Regle de reconstruction boot :
  • etat_absent_ou_incoherent (hors premier lancement clean) -> LOCKED_PERSISTENT : AUTORISEE et OBLIGATOIRE (fail-closed).

Aucune transition retour applicable vers etat nominal ; toutes les transitions inverses sont explicitement interdites.

5bis. Diagrammes Mermaid

Diagramme d'etats — Machine anti-tampering

Ref. INV-262-08 (transitions autorisees), INV-262-09 (etat terminal), INV-262-01 (fail-closed).

stateDiagram-v2
    [*] --> BootCheck : App launch

    state BootCheck <<choice>>
    BootCheck --> MONITORED : lockout_persistent_flag absent\n+ first_launch_clean present
    BootCheck --> LOCKED_PERSISTENT : lockout_persistent_flag=true\nOU donnee corrompue/absente\n(hors premier lancement clean)\n[INV-262-01 fail-closed]

    MONITORED --> TAMPERED_SESSION : Detection positive\nOU DETECTOR_ERROR\n[INV-262-01, INV-262-03]
    TAMPERED_SESSION --> LOCKED_PERSISTENT : Purge + lockout persiste\n[INV-262-05, INV-262-12]

    MONITORED --> MONITORED : Controle periodique OK\n[INV-262-03 trigger-coverage]

    note right of TAMPERED_SESSION
        Retour vers MONITORED INTERDIT
        [INV-262-08]
    end note

    note right of LOCKED_PERSISTENT
        Etat terminal — aucune sortie
        Resolution : reinstallation propre uniquement
        [INV-262-09]
    end note

Diagramme de sequence — Detection et lockout (Flux F3)

Ref. INV-262-02 (native-authority), INV-262-05 (purge-on-detect), INV-262-06 (local-first), INV-262-11 (no-sensitive-telemetry).

sequenceDiagram
    participant Timer as Controle periodique
    participant Native as Module natif iOS (Swift)
    participant Keychain as Keychain iOS
    participant Memory as Secrets memoire
    participant UI as Ecran lockout
    participant JS as Couche JS/RN
    participant Backend as Backend audit

    Timer->>Native: Declenchement controle<br/>[INV-262-03 cold start/foreground/periodique]
    activate Native

    Native->>Native: Controles multi-sources<br/>(jailbreak, Frida, codesign, dylib, sandbox)

    alt Detection positive OU DETECTOR_ERROR [INV-262-01]
        Native->>Native: tampered=true, reason_code defini

        Note over Native: Decision prise en natif uniquement<br/>[INV-262-02 native-authority]<br/>JS ne peut ni inhiber ni retarder

        Native->>Memory: Purge secrets, tokens, caches crypto,<br/>temporaires dechiffres, previews<br/>[INV-262-05]
        Memory-->>Native: Purge executee (ou partielle)

        alt Purge partielle [ERR-05]
            loop Max 3 retries, intervalle 1s
                Native->>Memory: Retry purge
                Memory-->>Native: Resultat retry
            end
        end

        Native->>Keychain: Ecriture lockout_persistent_flag=true<br/>(kSecAttrAccessibleAfterFirstUnlock)<br/>[INV-262-12]
        Keychain-->>Native: Confirmation persistance

        Native->>UI: Affichage ecran lockout non dismissable
        Native-->>JS: Bridge bloque — operations sensibles interdites

        Native->>Backend: POST /audit ANTI_TAMPERING_LOCKOUT<br/>(best effort, non bloquant)<br/>[INV-262-06 local-first]
        Note over Native,Backend: Payload : reason_code, device_id_pseudo,<br/>timestamp, app_version, environment<br/>Aucune donnee sensible [INV-262-11]

        alt Backend indisponible [ERR-04]
            Backend-->>Native: Timeout / erreur reseau
            Note over Native: Lockout maintenu, pas de rollback<br/>[INV-262-06]
        end
    else Aucune compromission
        Native-->>Timer: tampered=false, session continue
    end

    deactivate Native

6. Cas d'erreur

ID Cas Reponse attendue
ERR-01 Detecteur retourne erreur interne reason_code=DETECTOR_ERROR, fail-closed, lockout
ERR-02 Timeout budget controle depasse fail-closed, lockout, journalisation locale
ERR-03 Payload audit invalide (format §3.3) rejet envoi, lockout conserve, log local
ERR-04 Reseau indisponible pendant reporting lockout conserve, pas de retry bloquant UX
ERR-05 Purge partielle (un sous-ensemble inaccessible) lockout maintenu, retry purge locale borne (max 3, intervalle 1s), abandon apres 3 echecs avec log purge incomplete, aucune reprise normale
ERR-06 Feature flag QA incoherent (simulateur) ou tentative runtime de modification anti-tampering reste inactif en simulateur ; toute tentative runtime est ignoree car le flag QA est compile-time uniquement
ERR-07 Detection concurrente multiple causes 1 lockout unique, conserver cause primaire + causes secondaires en audit local si disponible
ERR-08 Donnee locale lockout corrompue fail-closed (LOCKED_PERSISTENT)
ERR-09 Donnee lockout absente au boot alors que l'app est deja initialisee et sans first_launch_clean fail-closed (LOCKED_PERSISTENT)
ERR-10 Process tue/crashe entre TAMPERED_SESSION et LOCKED_PERSISTENT au redemarrage, etat reconstruit en LOCKED_PERSISTENT

7. Criteres d'acceptation (testables)

ID Critere Observable
CA-01 Au cold start release sur device reel, un controle anti-tampering est execute avant acces aux secrets applicatifs. Traces d'audit local + absence d'acces si detection.
CA-02 A chaque retour foreground, un re-check est execute. Compteur de checks incremente par transition foreground.
CA-03 Le timer periodique respecte [30s..60s], defaut 45s. Mesure intervalle entre checks.
CA-04 Toute detection positive force tampered=true en <=1s P95. Timestamp detection vs lockout.
CA-05 TAMPERED_SESSION->MONITORED est impossible. Test de non-regression transitions.
CA-06 LOCKED_PERSISTENT->* est interdit, y compris apres redemarrage. Redemarrage conserve lockout.
CA-07 La purge couvre tous les elements listes en §2 Inclus. Verifications memoire/cache/temp post-detection.
CA-08 Les blobs locaux deja chiffres ne sont pas wipes automatiquement. Presence blobs chiffrees + store inaccessible.
CA-09 Le reporting backend ne contient aucune donnee sensible. Inspection payload : seuls champs §3.3 presents.
CA-10 Si backend indisponible, lockout/purge restent effectifs sans delai supplementaire. Test offline.
CA-11 En DEBUG et simulateur, module inactif ; en RELEASE/TestFlight/production actif ; en QA device reel, activation uniquement via build flag compile. Matrice environnement x comportement.
CA-12 Toute donnee du payload respecte regex/formats §3.3. Validation schema stricte.
CA-13 En cas d'erreur detecteur, comportement fail-closed applique. Injection faute detecteur.
CA-14 Contrainte INV-262-10 : aucun secret temporaire en clair au repos local. Audit stockage local de secrets temporaires.
CA-15 Frontiere purge/conservation explicite : caches crypto purgees, donnees persistantes chiffrees conservees. Verifications post-lockout des deux classes de donnees.
CA-16 Le lockout est persiste en Keychain avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly et re-applique apres reboot. Inspection metadonnees Keychain + comportement boot.
CA-17 Absence donnee lockout au boot (hors premier lancement clean) est traitee en fail-closed LOCKED_PERSISTENT. Simulation absence + flag initialisation.
CA-18 Si crash/kill pendant transition TAMPERED_SESSION -> LOCKED_PERSISTENT, le redemarrage force LOCKED_PERSISTENT. Test kill process au milieu de sequence.
CA-19 Retry purge borne: max 3 retries, intervalle 1s, puis abandon avec lockout maintenu et log purge incomplete. Compteur retries + logs + etat final lockout.

8. Scenarios de test (Given / When / Then)

  • ST-01 (F1/CA-01)
    Given app release sur iPhone reel non compromis
    When l'app demarre a froid
    Then le controle anti-tampering s'execute avant acces secrets et tampered=false.

  • ST-02 (F2/CA-02)
    Given app en background puis retour foreground
    When la transition foreground se produit
    Then un re-check est execute avant reprise des operations sensibles.

  • ST-03 (F3/CA-04/CA-07)
    Given environnement avec signal FRIDA_DETECTED
    When le controle detecte la compromission
    Then tampered=true en <=1s P95, purge immediate executee, lockout affiche.

  • ST-04 (F3/CA-05)
    Given etat TAMPERED_SESSION
    When une tentative de retour etat nominal est emise
    Then transition refusee.

  • ST-05 (F5/CA-06)
    Given etat LOCKED_PERSISTENT persiste
    When l'app est redemarree
    Then l'app reste verrouillee sans transition sortante.

  • ST-06 (F4/CA-10)
    Given compromission detectee et reseau indisponible
    When l'evenement audit est tente
    Then lockout/purge restent effectifs et aucune reprise normale n'est possible.

  • ST-07 (CA-11)
    Given matrice environnements (DEBUG, SIMULATOR, QA reel, RELEASE, TESTFLIGHT, PRODUCTION)
    When module anti-tampering est initialise
    Then activation correspond exactement aux regles INV-262-07, avec QA controle par build flag compile non mutable runtime.

  • ST-08 (CA-12)
    Given payload audit construit
    When validation schema est appliquee
    Then tout champ non conforme aux regex §3.3 est rejete.

  • ST-09 (CA-13)
    Given detecteur en erreur interne simulee
    When le controle est lance
    Then reason_code=DETECTOR_ERROR et lockout fail-closed.

  • ST-10 (CA-14)
    Given artefacts cryptographiques temporaires au repos local
    When audit de stockage est execute
    Then aucun secret temporaire n'existe en clair.

  • ST-11 (CA-15)
    Given lockout declenche avec presence de caches crypto et de blobs persistants deja chiffres
    When purge est executee
    Then caches crypto sont supprimes et blobs persistants chiffres sont conserves/inexploitables.

  • ST-12 (CA-16)
    Given lockout persistant active
    When inspection du stockage local est effectuee
    Then lockout_persistent_flag est en Keychain avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.

  • ST-13 (CA-17)
    Given app deja initialisee et donnee lockout absente au boot sans first_launch_clean
    When application demarre
    Then etat force a LOCKED_PERSISTENT (fail-closed).

  • ST-14 (CA-18)
    Given process tue entre TAMPERED_SESSION et LOCKED_PERSISTENT
    When application redemarre
    Then etat reconstruit en LOCKED_PERSISTENT.

  • ST-15 (CA-19)
    Given purge partielle persistante
    When mecanisme de retry purge s'execute
    Then max 3 retries a 1s, puis abandon avec lockout maintenu et log purge incomplete.

9. Hypotheses explicites

ID Hypothese Impact si faux
H-01 Le device_id_pseudo est derive de facon deterministe par SHA-256 de UIDevice.current.identifierForVendor.uuidString (UTF-8). Incompatibilite backend/audit si autre derive utilise.
H-02 Le lockout persistant local est considere suffisant jusqu'a reinstallation propre. Besoin d'un mecanisme additionnel si exigence support differente.
H-03 Le budget cold start P95 1500ms est acceptable produit. Si trop strict/large, risque faux positifs ou degradation UX.
H-04 Le payload audit minimal suffit aux besoins forensic initiaux. Si insuffisant, evolution schema et versioning necessaires.
H-05 Les blobs locaux conserves sont effectivement inutilisables sans secrets purges. Si faux, risque de recuperation indirecte de donnees.

10. Points a clarifier

10.1 Contraintes techniques (obligatoire)

  • Projet cible identifie : ProbatioVault-app.
  • Stack contractuelle : React Native + Expo SDK 54 + TypeScript.
  • Contrainte module : logique critique anti-tampering executee en natif iOS (Swift/Obj-C) avec exposition minimale au runtime RN.
  • Toute reference a une stack non conforme (ex: SwiftUI-only app, Spring Boot) est non valide pour cette story.

10.2 Donnees manquantes / decisions ouvertes

ID Point Consequence
Q-01 Reference epique exacte non fournie (Reference epique vide). Impossible de finaliser metadata documentaire.
Q-02 {DOMAINE} et {PD-262-nom} cibles non fournis pour le chemin d'artefact. Chemin final de depot non resolu.
Q-03 Politique precise de retry backend (nombre max, backoff, retention locale) non definie. Comportement offline heterogene possible.
Q-04 Niveau de detail des causes secondaires en audit local non contractualise. Variabilite d'observabilite incident.
Q-05 Liste exhaustive des emplacements de caches/temporaires a purger non versionnee. Risque de couverture purge incomplete.

References

  • Epic : A renseigner (non fourni)
  • JIRA : PD-262
  • Repos concernes : ProbatioVault-app (principal), backend (reception audit best effort)
  • Documents associes :
  • Besoin PD-262 (document complementaire fourni)
  • REX PD-174 (S-01/S-02)
  • PD-98 (Keychain Kmaster)
  • PD-107 (Biometric auth)
  • PD-97 (Hermes limitations)
  • PD-39 (fail-closed)