Une architecture à connaissance nulle : le serveur ne détient de clés de déchiffrement que pour les identifiants techniques. Tout champ porteur de sens — numéros de téléphone, adresses, coordonnées, secrets de webhook, BSSID Wi-Fi, messages d’accueil — est chiffré sur l’appareil du propriétaire et repose sur le serveur sous forme de blob opaque. La clé de déchiffrement parvient à l’invité dans le lien d’invitation, dans le fragment d’adresse, que le navigateur n’envoie jamais au serveur.
Algorithm: AES-256-GCM. Disponible d’origine sur la JVM (via javax.crypto),
et pris en charge par l’Android Keystore. Il apporte à la fois confidentialité et authentification.
v1:<base64url(nonce || ciphertext || tag)>
v1: — un préfixe de version, pour changer le format plus tard.nonce — 12 octets aléatoires, renouvelés à chaque chiffrement.ciphertext+tag — tout ce que renvoie Cipher.doFinal ; l’étiquette de 128 bits est incluse.SecureRandom() (celui du système). Sur Android, c’est /dev/urandom + le prng de Linux.
EncryptedSharedPreferences sous le nom master_key et ne quitte jamais l’appareil en clair.K_obj pour le stockage local.encrypt_k_master(K_obj) sous la clé obj_key_{id}.data_cipher sur le serveur).K_obj_new → rechiffrer data_cipher et tous les lots qui le portaient.{obj_id: K_obj} pour les objets autorisés à l’invité.bundle_cipher. La puce K_guest lui-même n’est jamais vu par le serveur.entrixy.com/key#<user_key><base64url(K_guest)> — un unique bloc de 75 caractères.
Le fragment ne part pas au serveur avec la requête HTTP : c’est une garantie de base du navigateur.K_guest n’est possible qu’en émettant un nouveau lien. C’est précisément là-dessus que repose la révocation :
le serveur supprime l’enregistrement de la clé et l’invité ne reçoit plus de bundle_cipher à jour.| Key | Valeur |
|---|---|
master_key | 32 octets en clair (SharedPreferences les chiffre de toute façon). |
obj_key_{id} | La K_obj de l’objet, enveloppée par K_master. |
obj_plain_{id} | (facultatif) un cache du JSON déchiffré, pour que l’interface s’affiche vite. |
| Table / champ | Sommaire |
|---|---|
numbers.data_cipher | JSON chiffré avec K_obj : {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}. |
numbers.id / type | En clair — nécessaire au routage et à l’affichage du type d’icône. |
keys.bundle_cipher | JSON chiffré avec K_guest : {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}. |
keys.user_key / mode / force_when_busy | En clair — pour le routage et une vérification rapide de l’accès. |
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>
user_key et 43 caractères de la clé invité.user_key — l’identifiant côté serveur. Sans lui, le serveur ne livre pas le lot.#...) n’atteint jamais le journal du serveur — c’est une garantie du navigateur./key — du JS ordinaire : elle lit location.hash, analyse le bloc et transmet la clé
soit à l’application, soit à la version web /app/. Le serveur ne voit jamais la clé./download/android,
d’où l’application récupère la clé toute seule.K_obj.K_obj → data_cipher.POST /api/number_add avec data_cipher plus le type. Le serveur renvoie un id.obj_key_{id} = encrypt_k_master(K_obj).Construire le nouveau JSON localement, le chiffrer avec la K_obj → POST /api/number_update.
K_obj existante, qui ne change pas.
[obj1, obj2, obj3]. Cela génère K_guest.{obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.K_guest → bundle_cipher.POST /api/key_create avec bundle_cipher, obj_ids (en clair — pour la matrice d’accès).user_key.entrixy.com/key#{user_key}{base64url(K_guest)}./key reads location.hash et transmet la clé à l’application ou à la version web.POST /api/key_bundle.php avec user_key → reçoit bundle_cipher et la liste des obj_ids.K_guest→ obtenant {obj_keys, welcome}.K_guest et les déchiffrés obj_keys sont stockés dans EncryptedSharedPreferences du client invité.data_cipher de l’objet → déchiffrement avec la obj_key_{id}.Le propriétaire appuie sur Supprimer → POST /api/key_revoke.php → le serveur supprime l’enregistrement de la clé.
À la prochaine demande de lot, l’invité est refusé et perd l’accès.
Le propriétaire appuie sur Régénérer la clé dans les réglages de l’objet. Cela génère K_obj_new,
et tous les bundle_cipher contenant cet objet sont rechiffrés. Le client ne les connaît pas par cœur,
aussi le client du propriétaire reconstruit les lots : il récupère le bundle_cipher de chacune de ses
clés, le déchiffre, insère K_obj_newet rechiffre.
Les nouveaux blobs partent au serveur en un seul lot.
Losing K_master signifie perdre l’accès à tous les objets et à toutes les clés, d’où la nécessité d’un export.
K_master → produisant master_backup_blob.obj_keys depuis le serveur — ils se trouvent dans le lot des propres clés du propriétaire — et les rechiffre localement.K_master, c’est pourquoi la phrase secrète est obligatoire.Add Crypto.kt : une enveloppe AES-256-GCM, génération et stockage de K_master,
des utilitaires pour K_obj, sérialisation du format v1:<base64url>.
Tests unitaires : chiffrer → déchiffrer → comparer.
Rien ne part encore au serveur — la tuyauterie est simplement prête.
Tous les champs sensibles ont migré vers numbers.data_cipher et user_keys.bundle_cipher.
Les anciennes colonnes en clair (phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip) ont été supprimées de la base (phase 7).
webhook_url et webhook_secret restent en clair UNIQUEMENT quand webhook_mode='server' — sans eux le serveur ne peut pas envoyer la requête HTTP. Avec webhook_mode='phone' ils sont chiffrés dans data_cipher.
À la création ou à la mise à jour, le client chiffre le JSON et envoie data_cipher. À la synchronisation, il lit le blob et le déchiffre. Les nouveaux objets vivent entièrement dans le blob ; les anciens sans K_obj continuent d’utiliser les champs en clair pour la rétrocompatibilité.
La création d’une clé génère K_guest, assemble le lot et envoie bundle_cipherau serveur.
Le lien porte la clé dans son fragment ; la page /key lit le hash et transmet la clé à l’application.
Un écran de réglages avec Exporter la clé maîtresse : phrase secrète → Argon2id → QR code. Restauration via le scanner, plus des tests de migration entre appareils. Non réalisé.
Une routine unique dans le client au premier lancement de la nouvelle version :
K_obj pour chacun d’eux.data_cipher.phone=NULL, …) — mais seulement après confirmation.Un audit de sécurité, externe ou interne. On vérifie que le serveur ne voit vraiment pas les données sensibles : un dump de la base ne doit contenir que des blobs.
| Risk | Measure |
|---|---|
| Perte de K_master | Un parcours de sauvegarde obligatoire (phase 5). Tant qu’aucune copie n’existe, un bandeau d’avertissement s’affiche. |
| Compromission de l’appareil du propriétaire | Pas totalement évitable : l’entrée dans l’application est protégée par un code PIN ou la biométrie, avec EncryptedSharedPreferences et l’Android Keystore. |
| Faire tourner la clé d’un objet impose de reconstruire tous les lots | Le client du propriétaire prend tous les bundle_cipher, en local ou depuis le serveur, les reconstruit et les envoie en un lot. Opération rare. |
| Le serveur altère un lot | L’étiquette AES-GCM ne correspondra pas et le client signalera une clé corrompue. |
| Fuite de K_guest par une capture d’écran du QR code | Techniquement inévitable : la clé est le code lui-même. Ne montrez le QR code qu’à un invité de confiance. |
| Le fragment d’adresse reste dans l’historique du navigateur | La page /key appelle, juste après avoir lu le hash, history.replaceState(..., '#'), ce qui efface le fragment. |
Dernière mise à jour : 19 avril 2026