Où Casa Tookan garde les phrases de récupération et les clés privées, qui peut les déverrouiller, et comment elles servent à signer, depuis la version 2.1.0.
Tout ce qui est secret appartient au signataire. L’app ne voit que des données qui ne permettent pas de dépenser.
| Quoi | Où cela vit | Qui peut le lire |
|---|---|---|
| Phrases de récupération, clés privées | Enregistrements de domaine scellés (keychain / fichiers privés de l’app) | Le signataire natif, après authentification du propriétaire |
| La clé qui les scelle (DEK) | Enveloppée deux fois : par le verrouillage du système, et par le code PIN du portefeuille | Le système après Face ID / empreinte / code ; le signataire après un code PIN correct |
| Code PIN du portefeuille | Nulle part (seule une clé qui en est dérivée, mélangée à un secret matériel, est conservée) | Personne |
| Compteur d’échecs du code PIN | Keychain / fichier privé, protégé par HMAC | Le signataire natif |
| xpubs, adresses, liste des domaines | Métadonnées du signataire + stockage de l’app | Quiconque a le téléphone déverrouillé ; ces données ne permettent pas de dépenser |
| Noms, couleurs et devises des casas | Stockage de l’app (AsyncStorage) | L’app |
L’app peut demander une signature. Seul le signataire peut signer, une fois que le propriétaire a vu, sur l’écran du signataire lui-même, ce que le cœur a décodé des octets exacts à signer.
signer-core.js)require.Une seule clé de données aléatoire scelle tous les domaines. Cette clé est enveloppée deux fois : perdre un accès ne fait jamais perdre le portefeuille.
À la configuration, le signataire tire une clé de chiffrement des données (DEK) aléatoire de 32 octets. Chaque domaine (une phrase de récupération, ou les clés importées) devient un enregistrement scellé en AES-256-GCM sous la DEK. Les données authentifiées lient l’enregistrement à son domaine : déplacé vers un autre identifiant de domaine, il ne se déchiffre plus.
record = AES-256-GCM( DEK, iv, recordJSON,
aad = "casa-signer:v1:estate:" + estateId )
Un enregistrement contient la phrase et ses seeds précalculées (l’étirement BIP-39 est trop lent dans un moteur sans JIT), ou les clés importées.
dek.os : le verrouillage du système.userPresence, accessible WhenPasscodeSetThisDeviceOnly. iOS le supprime si le code est retiré.dek.pin : code PIN + matérielKEK = HKDF-SHA256( PBKDF2-SHA256(PIN, salt, 600 000) ‖ hw,
info = "casa-signer:v1:pin-kek" )
Le code PIN n’est jamais stocké. Les mauvaises tentatives sont comptées en natif : les 5 premières sont libres, puis chaque échec bloque le code PIN 30 s, durée doublée à chaque fois jusqu’à 1 h. Le compteur est protégé par HMAC avec une clé propre à l’appareil : ni le JavaScript de l’app ni une sauvegarde restaurée ne peuvent le remettre à zéro.
Chaque configuration écrit les enregistrements scellés, puis les deux enveloppes, relit tout et compare octet par octet. C’est seulement ensuite qu’il écrit meta, la description publique du portefeuille. Pas de meta signifie rien ici : les restes d’une configuration interrompue sont effacés au lancement suivant.
| iOS | Android | |
|---|---|---|
Enregistrements, enveloppes, compteur, meta | Keychain, classe Internet password, serveur casa-signer.local, ThisDeviceOnly | Fichiers dans noBackupFilesDir/casa-signer/ |
| Clés matérielles | Secure Enclave (enveloppe PIN), contrôle d’accès du keychain (enveloppe système) | AndroidKeyStore, StrongBox si présent |
| Sauvegardes, autres appareils | Jamais (ThisDeviceOnly) | Jamais (répertoire exclu des sauvegardes, clés liées au matériel) |
Le JavaScript de l’app n’a aucun module de keychain ou de keystore. Sur iOS, les éléments du signataire utilisent une classe de keychain que les stockages de mots de passe génériques (ceux qu’utilisent les modules React Native) ne peuvent pas atteindre.
Pour chaque domaine, le signataire ne publie que des données publiques. L’app en dérive chaque adresse (src/signer/publicKeys.ts) ; elle peut afficher les soldes et construire des transactions. Pour dépenser, il faut passer par le signataire.
m/0'/0m/84'/0'/0'/0m/86'/0'/0'/0m/44'/60'/0'/0…/hd.forbidden.test.ts) fait échouer le build si le code de l’app importe BIP-39, dérive depuis une seed, touche une clé privée, signe une transaction ou ouvre un stockage de secrets.prepare décode les octets exacts : montants, destinataires, monnaie rendue, frais, le message Counterparty, les autorisations de tokens, l’adresse de signature. Le contexte fourni par l’app (nom du site, symbole du token, nom de la casa) est marqué non vérifié.sign décode à nouveau la requête et refuse (E_DIGEST_MISMATCH) si ce qu’il signerait diffère de ce qui a été affiché.Un écran et une authentification couvrent un lot entier, par exemple plusieurs PSBT ou une connexion multi-chaînes. Une requête à la fois, trois au plus en dix secondes.
Réglages › Phrase de récupération et clés demande au signataire d’authentifier le propriétaire et d’afficher la phrase ou les clés d’une casa sur son propre écran : captures et enregistrement d’écran sont bloqués, et il se masque au bout de 60 s. Le secret n’est jamais renvoyé à l’app.
Les deux s’exécutent en natif : la phrase ne passe jamais par le JavaScript de l’app. Une migration interrompue avant sa validation recommence simplement ; si l’ancien code PIN est oublié, l’utilisateur efface ce portefeuille et le restaure depuis la phrase de récupération.
Le signataire lit l’ancien coffre, demande l’ancien code PIN sur son propre pavé (en conservant l’ancien compteur de tentatives), déchiffre le domaine principal et les domaines supplémentaires, les scelle sous une nouvelle DEK, vérifie, valide, puis supprime toutes les anciennes entrées.
Le signataire lit l’ancien enregistrement keychain / keystore, vérifie l’ancien code PIN à 4 chiffres (avec limitation), demande un nouveau code PIN, scelle, vérifie, valide et supprime l’ancien enregistrement. Les noms des casas et la devise sont lus à part : ils ne sont pas secrets.
| Menace | Résultat | |
|---|---|---|
| Une dépendance npm compromise dans l’app | Ne peut lire aucun secret ; peut seulement demander des signatures, chacune affichée et confirmée en natif. | Couvert |
| Une dApp qui demande de signer quelque chose de trompeur | Le signataire montre ce que font réellement les octets ; les autorisations illimitées, permits et signatures partielles sont signalés. | Couvert |
| Un téléphone verrouillé volé | Les enregistrements exigent le verrouillage du système, ou le code PIN plus le matériel du téléphone ; les essais de code PIN sont limités. | Couvert |
| Une sauvegarde ou des fichiers copiés | Rien n’est dans les sauvegardes ; il manque aux fichiers copiés les clés matérielles. | Couvert |
| Retirer le verrouillage d’écran | Le portefeuille continue de s’ouvrir avec son code PIN. | Couvert |
| Quelqu’un qui connaît le code de l’appareil et tient le téléphone déverrouillé | Peut signer : le code de l’appareil sert d’authentification, comme pour Apple Pay. | Voulu |
| Un système compromis (jailbreak, root) | Hors périmètre. | Non couvert |
| Perdre le téléphone sans la phrase de récupération | Les fonds sont perdus : les clés n’existent que sur ce téléphone. | Non couvert |
signer-core/src/ → npm run signer:build ; signer:check tourne avant les tests et échoue si le bundle dérive.modules/casa-signer/ios/Custody.swift, …/android/…/Custody.kt.npm run signer:native-test exécute la garde et le cœur sur macOS.modules/casa-signer/mock.ts : le vrai cœur bundlé dans une VM Node, avec la garde en mémoire. Ce mock n’est jamais intégré à l’app.