Stockage des clés

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.

En une phraseLes phrases de récupération et les clés privées n’entrent jamais dans le JavaScript de l’app. Elles vivent dans le signataire natif, chiffrées sur le téléphone sous une clé que seuls le Face ID, l’empreinte ou le code de l’appareil du propriétaire, ou à défaut le code PIN du portefeuille, peuvent libérer, et elles ne sont utilisées que dans un moteur isolé qui exécute un cœur de signature figé et relu.

En bref

Tout ce qui est secret appartient au signataire. L’app ne voit que des données qui ne permettent pas de dépenser.

QuoiOù cela vitQui peut le lire
Phrases de récupération, clés privéesEnregistrements 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 portefeuilleLe système après Face ID / empreinte / code ; le signataire après un code PIN correct
Code PIN du portefeuilleNulle 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 PINKeychain / fichier privé, protégé par HMACLe signataire natif
xpubs, adresses, liste des domainesMétadonnées du signataire + stockage de l’appQuiconque a le téléphone déverrouillé ; ces données ne permettent pas de dépenser
Noms, couleurs et devises des casasStockage de l’app (AsyncStorage)L’app

Trois couches

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.

App (React Native / Hermes)

Pas de confiance pour les clés
  • Affiche les soldes, construit des transactions non signées.
  • Dérive chaque adresse des xpubs que publie le signataire.
sign(request)signature seule

Signataire natif (Swift / Kotlin)

De confiance
  • Garde : la DEK, les enregistrements scellés, l’enveloppe PIN, le compteur de tentatives.
  • Authentification : Face ID / empreinte / code de l’appareil, ou son propre pavé PIN.
  • Ses propres écrans de confirmation, où les captures et l’enregistrement d’écran sont bloqués.
contexte neuf à chaque opération

Cœur du signataire (signer-core.js)

Moteur isolé
  • iOS : une VM JavaScriptCore dédiée · Android : JavaScriptSandbox (QuickJS en repli).
  • noble / scure 2.2.0 uniquement, sans réseau, sans timers et sans require.
  • Son SHA-256 est compilé dans le binaire natif : tout autre contenu est refusé.

Modèle de garde

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.

1 · Une clé de données, des enregistrements scellés

À 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.

2 · La DEK est enveloppée deux fois

Au quotidien

dek.os : le verrouillage du système

  • iOS : élément de keychain avec le contrôle d’accès .userPresence, accessible WhenPasscodeSetThisDeviceOnly. iOS le supprime si le code est retiré.
  • Android : la DEK sous une clé RSA-2048 de l’AndroidKeyStore (OAEP / SHA-256) qui exige une authentification (biométrie forte ou identifiant de l’appareil). StrongBox si disponible. Pas invalidée par l’ajout d’une empreinte ; invalidée si le verrouillage d’écran est retiré. À partir de l’API 30, chaque usage est lié à l’invite ; les API 26–29 laissent 10 s après l’authentification.
Repli

dek.pin : code PIN + matériel

  • Utilisée quand le téléphone n’a pas de verrouillage d’écran, ou a perdu son enveloppe système.
  • iOS : la part matérielle est un secret ECDH issu d’une clé de la Secure Enclave qui ne quitte jamais la puce (un pepper en keychain à défaut d’enclave).
  • Android : la DEK enveloppée par le code PIN est scellée à nouveau par une clé AES non exportable du Keystore.
  • Un fichier copié ne peut pas être attaqué par force brute hors du téléphone : la part matérielle manque.
KEK = HKDF-SHA256( PBKDF2-SHA256(PIN, salt, 600 000) ‖ hw,
                   info = "casa-signer:v1:pin-kek" )
Pourquoi deux enveloppes ?Avec la seule enveloppe système, retirer le verrouillage d’écran détruirait le portefeuille. Avec l’enveloppe PIN en repli, le portefeuille continue de s’ouvrir avec son code PIN, et Réglages › Réutiliser le verrouillage d’écran réenveloppe la DEK une fois un verrouillage rétabli.

3 · Tentatives de code PIN

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.

4 · Le point de validation

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.

Où sont les octets

iOSAndroid
Enregistrements, enveloppes, compteur, metaKeychain, classe Internet password, serveur casa-signer.local, ThisDeviceOnlyFichiers dans noBackupFilesDir/casa-signer/
Clés matériellesSecure Enclave (enveloppe PIN), contrôle d’accès du keychain (enveloppe système)AndroidKeyStore, StrongBox si présent
Sauvegardes, autres appareilsJamais (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.

Ce que l’app détient

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.

xpubs de compte

  • Bitcoin legacy m/0'/0
  • Bitcoin segwit m/84'/0'/0'/0
  • Bitcoin taproot m/86'/0'/0'/0
  • EVM m/44'/60'/0'/0
  • Chaque casa est un enfant non durci : …/hd.

Aussi

  • Les clés publiques des clés importées.
  • Une empreinte, pour refuser une phrase ou une clé ajoutée deux fois.
  • Un test garde-fou (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.

La signature, de bout en bout

  1. L’app construit une requête non signéeUne PSBT, une transaction EVM avec nonce, gas et frais déjà renseignés, un message, des données typées.
  2. Le cœur prépare ce qui sera affichéDans un contexte neuf, 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é.
  3. Le propriétaire confirme et s’authentifieSur l’écran du signataire lui-même, par un appui long, puis le verrouillage du système ou le code PIN.
  4. Le cœur signe ou refuseLa DEK est désenveloppée et l’enregistrement déchiffré. 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é.
  5. Seul le résultat repartLa signature ou la transaction signée. Les secrets et la DEK sont jetés avec le contexte.

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.

Afficher les secrets

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.

Migrations

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.

Build ≤ 44

Coffre JS (expo-secure-store)

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.

Titanium ≤ 1.2.0

L’app d’origine

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.

Contre quoi cela protège

MenaceRésultat
Une dépendance npm compromise dans l’appNe 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 trompeurLe 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ésRien n’est dans les sauvegardes ; il manque aux fichiers copiés les clés matérielles.Couvert
Retirer le verrouillage d’écranLe 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érationLes fonds sont perdus : les clés n’existent que sur ce téléphone.Non couvert

Pour les développeurs

Sources

  • Cœur : signer-core/src/ → npm run signer:build ; signer:check tourne avant les tests et échoue si le bundle dérive.
  • Garde : modules/casa-signer/ios/Custody.swift, …/android/…/Custody.kt.

Tests

  • Sur un téléphone : Outils de développement › Signer self-test.
  • npm run signer:native-test exécute la garde et le cœur sur macOS.
  • Jest utilise 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.