Skip to content

Latest commit

 

History

History
405 lines (305 loc) · 18.1 KB

File metadata and controls

405 lines (305 loc) · 18.1 KB
🇫🇷 Français | 🇬🇧 English

🔐 Protocole Cryptographique


Vue d'ensemble

Alice                                         Bob
  │                                             │
  │◄──────── Échange clés publiques ──────────►│
  │           (QR code v2 : X25519 + ML-KEM)    │
  │                                             │
  │  shared_secret = X25519(sk_A, pk_B)         │  shared_secret = X25519(sk_B, pk_A)
  │  root_key = HKDF(shared_secret)            │  root_key = HKDF(shared_secret)
  │  send_chain = HKDF(root, "init-send")      │  recv_chain = HKDF(root, "init-send")
  │  recv_chain = HKDF(root, "init-recv")      │  send_chain = HKDF(root, "init-recv")
  │                                             │
  │  ┌─ PQXDH (premier message) ────────────┐  │
  │  │ kem_ct = ML-KEM-1024.Encaps(pk_kem_B)   │  │
  │  │ kem_ss = shared secret ML-KEM          │  │
  │  │ root_key' = HKDF(root_key || kem_ss)   │  │
  │  └──────────────────────────────────────┘  │
  │                                             │
  │  msg_key = HMAC(send_chain, 0x01)          │
  │  send_chain = HMAC(send_chain, 0x02)       │
  │  ct = AES-GCM(msg_key, iv, plaintext)      │
  │                                             │
  │  ──── {ct, iv, ephKey, kemCiphertext} ────►│
  │           (Tor P2P .onion)                  │
  │                                             │
  │                                             │  kem_ss = ML-KEM-1024.Decaps(sk_kem_B, kem_ct)
  │                                             │  root_key' = HKDF(root_key || kem_ss)
  │                                             │  msg_key = HMAC(recv_chain, 0x01)
  │                                             │  recv_chain = HMAC(recv_chain, 0x02)
  │                                             │  plaintext = AES-GCM-dec(msg_key, iv, ct)

Identité

  1. Seed Ed25519 (32 bytes) généré au premier lancement — secret maître unique
  2. Seed → Android Keystore (StrongBox si disponible)
  3. À partir du seed, dérivation de :
    • Ed25519 paire de clés (signature + identité maître)
    • Account ID = SHA3-256(pubkey) → Base58 (ex : Fa3x...9Z)
    • Adresse .onion (encodage Tor v3 depuis la pubkey Ed25519)
    • X25519 clé DH (birational map Edwards → Montgomery)
    • ML-KEM-1024 paire de clés (HKDF(seed, "fialka-ml-kem", 64 bytes) → KeyGen déterministe)
    • Empreinte = SHA-256 → 16 emojis (96-bit)
  4. Clé publique → Base64 + QR code pour partage
  5. Backup : seed → 24 mots BIP-39 (256 bits + 8-bit checksum SHA-256)
  6. Restauration : 24 mots → seed → re-dérive toutes les clés (Ed25519, X25519, ML-KEM, .onion)

Échange de clés

  1. Alice montre son QR code (ou partage sa clé publique)
  2. Bob scanne le QR → le pseudo d'Alice est pré-rempli automatiquement → crée le contact
  3. Chaque côté calcule : shared_secret = X25519(ma_clé_privée, clé_publique_contact)
  4. Le rôle (initiator/responder) est déterminé par l'ordre lexicographique des clés publiques
  5. Le QR v2 encode aussi la clé publique ML-KEM-1024 pour l'upgrade PQXDH

Format QR v2 : fialka://contact?key=<X25519_base64>&kem=<ML-KEM-1024_base64>&name=<displayName>


Fingerprint Emojis (96-bit, anti-MITM)

Chaque conversation a une empreinte partagée calculée à partir des deux clés publiques :

sorted_keys = sort_lexicographic(pubKeyA, pubKeyB)
hash = SHA-256(sorted_keys[0] + sorted_keys[1])
fingerprint = 16 emojis choisis dans une palette de 64
            = 16 × log2(64) = 96 bits d'entropie

Format : 🔥🐱🦄🍕 🌟🚀💎⚡ 🎸📱🔔🎉 🌈🐶🎯🍀 (4 × 4 emojis)

Les deux téléphones calculent la même empreinte. L'utilisateur compare visuellement (en personne ou par appel vidéo) pour détecter une attaque MITM.

  • ✅ Palette de 64 emojis (puissance de 2 → zéro biais modulo)
  • ✅ 96 bits d'entropie (7.9 × 10²⁸ combinaisons)
  • ✅ Badge dans le chat : ✅ Vérifié / ⚠️ Non vérifié
  • ✅ Vérification indépendante par utilisateur (état local Room uniquement)
  • ✅ Messages système dans le chat lors de la vérification/retrait (avec lien cliquable "Voir l'empreinte")
  • ✅ Notification événementielle via Tor P2P — notifie le pair, ne synchronise pas l'état

QR Code Fingerprint (V3.4.1)

En plus de la comparaison visuelle des emojis, les utilisateurs peuvent vérifier l'empreinte via QR code :

sorted_keys = sort_lexicographic(pubKeyA, pubKeyB)
hash = SHA-256(sorted_keys[0] + sorted_keys[1])
qr_data = hex(hash)   // 64 caractères ASCII (a-f0-9)
  • ✅ Le QR encode le SHA-256 en hexadécimal (pas les emojis) pour éviter les problèmes d'encodage Unicode
  • ✅ Méthode getSharedFingerprintHex() dans CryptoManager
  • ✅ Scanner utilise CustomScannerActivity (identique à l'invitation de contact)
  • ✅ Comparaison hex ignoreCase = true (case-insensitive)
  • ✅ Vérification automatique : scan → match → dialogue ✅ ; mismatch → dialogue ❌ MITM

Double Ratchet (PFS + Healing)

Initialisation (à l'acceptation du contact) :
  root_key     = HKDF(shared_secret, "Fialka-DR-root")
  send_chain   = HKDF(root_key, "Fialka-DR-chain-init-send")
  recv_chain   = HKDF(root_key, "Fialka-DR-chain-init-recv")  (swapped pour responder)
  ephemeral    = X25519.generateKeyPair()

Pour chaque message N (KDF chain) :
  message_key[N]  = HMAC-SHA256(chain_key[N], 0x01)   ← clé unique
  chain_key[N+1]  = HMAC-SHA256(chain_key[N], 0x02)   ← avancement irréversible

DH Ratchet (healing) — quand l'éphémère remote change :
  dh_secret    = X25519(local_ephemeral_priv, remote_ephemeral_pub)
  new_root_key = HKDF(root_key || dh_secret, "root-ratchet")
  new_chain    = HKDF(root_key || dh_secret, "chain-ratchet")
  → Nouvelle clé éphémère locale générée

  plaintext → pad(plaintext) → AES-256-GCM(message_key[N], random_iv_12B) → ciphertext

Padding (anti analyse de taille)

Avant chiffrement, chaque message est paddé vers le bucket supérieur :

Taille message Bucket
≤ 256 B 256 B
≤ 1 KB 1 KB
≤ 4 KB 4 KB
> 4 KB 16 KB
  • Header : 2 octets (Big-Endian) = longueur réelle du plaintext
  • Remplissage : SecureRandom bytes jusqu'à la taille du bucket
  • Suppression du padding à la réception via le header 2 octets

Propriétés

  • ✅ Chaque message a sa propre clé de chiffrement (KDF chain)
  • ✅ L'avancement de la chaîne est irréversible (one-way function)
  • Healing : compromission d'une chain key → DH ratchet guérit au prochain échange
  • ✅ Compromettre la clé actuelle ne révèle pas les clés passées
  • ✅ Les clés intermédiaires sont zérorisées de la mémoire après usage
  • ✅ HKDF IKM, PRK et expandInput zérorisés après chaque dérivation
  • ✅ Encodage/décodage mnemonic zérorise tous les tableaux d'octets intermédiaires et nettoie le StringBuilder
  • ✅ Clés éphémères X25519 renouvelées à chaque changement de direction

Signature de message (Ed25519, V3.2)

Chaque message est signé avec une clé Ed25519 dédiée (séparée de la clé d'identité X25519) via Fialka-Core (Rust, JNI).

Envoi :
  signingKeyPair = getOrDeriveSigningKeyPair()   (FialkaSecurePrefs)
  dataToSign = ciphertext.UTF8 || conversationId.UTF8 || createdAt.bigEndian8bytes
  signature  = Ed25519.sign(signingKeyPair.private, dataToSign)
  → envoyé dans le message via Tor : { ..., "signature": Base64(signature) }

Réception :
  signingPublicKey = fetchSigningPublicKeyByIdentity(contact.publicKey)
  dataToVerify = ciphertext.UTF8 || conversationId.UTF8 || createdAt.bigEndian8bytes
  valid = Ed25519.verify(signingPublicKey, dataToVerify, signature)
  → Badge : ✅ (valid=true) ou ⚠️ (valid=false ou clé absente)

Propriétés

  • Anti-falsification : seul le détenteur de la clé privée Ed25519 peut signer
  • Anti-replay inter-conversation : conversationId inclus dans les données signées
  • Anti-manipulation temporelle : createdAt (timestamp client) inclus dans les données signées
  • Clé de signature séparée de la clé d'identité X25519 (pas de mélange des usages)
  • Nettoyage : clé de signature effacée localement à la suppression du compte

PQXDH — Upgrade Post-Quantique (V3.4)

Fialka implémente un échange de clés hybride combinant X25519 (classique) et ML-KEM-1024 (post-quantique) via le protocole PQXDH.

Principe

À l'ajout du contact (QR scan) :
  Les clés publiques X25519 ET ML-KEM-1024 sont échangées via le QR code v2.
  La conversation démarre en mode classique X25519 uniquement (root_key classique).

Premier message (initiator) :
  kem_ct, kem_ss = ML-KEM-1024.Encaps(contact_kem_publicKey)
  root_key' = HKDF(root_key || kem_ss, "pqxdh-upgrade")
  → message via Tor inclut { ..., "kemCiphertext": Base64(kem_ct) }
  → root_key upgradé localement (chains recalculées)

Réception du premier message (responder) :
  kem_ss = ML-KEM-1024.Decaps(my_kem_privateKey, kemCiphertext)
  root_key' = HKDF(root_key || kem_ss, "pqxdh-upgrade")
  → root_key upgradé localement (chains recalculées)

Messages suivants :
  kemCiphertext n'est plus envoyé (upgrade unique)
  Double Ratchet continue avec root_key' (hybride)

Propriétés

  • Résistance post-quantique : même si X25519 est cassé par un ordinateur quantique, ML-KEM-1024 protège le root_key
  • Upgrade différée : pas de message bootstrap — l'upgrade se fait au premier vrai message
  • Aucune régression : si ML-KEM échoue, la conversation reste protégée par X25519 classique
  • Fialka-Core (Rust) : implémentation ML-KEM-1024 native, byte-for-byte compatible avec BouncyCastle 1.83 (migration transparente)
  • StrongBox probe : DeviceSecurityManager détecte le support matériel StrongBox pour la protection des clés

SPQR — Ré-encapsulation PQ périodique (V3.5)

Après l'upgrade PQXDH initiale, le Double Ratchet classique reprend avec des échanges X25519 uniquement. SPQR (Supplementary Post-Quantum Ratchet) ajoute une ré-encapsulation ML-KEM-1024 périodique pour maintenir la résistance post-quantique au fil du temps.

Principe

Toutes les PQ_RATCHET_INTERVAL = 10 messages envoyés :

Sender (Alice) :
  kem_ct, kem_ss = ML-KEM-1024.Encaps(contact_kem_publicKey)
  root_key' = HKDF(root_key, kem_ss, info="Fialka-SPQR-pq-ratchet")
  → message via Tor inclut { ..., "kemCiphertext": Base64(kem_ct) }
  → pqRatchetCounter remis à 0

Receiver (Bob) :
  Si pqxdhInitialized ET kemCiphertext présent ET pas un PQXDH initial :
    kem_ss = ML-KEM-1024.Decaps(my_kem_privateKey, kemCiphertext)
    root_key' = HKDF(root_key, kem_ss, info="Fialka-SPQR-pq-ratchet")
    → pqRatchetCounter remis à 0

Propriétés

  • Healing PQ continu : même si un secret PQ est compromis, il est renouvelé 10 messages plus tard
  • Rétrocompatibilité : réutilise le champ kemCiphertext existant (distingué du PQXDH initial par pqxdhInitialized)
  • Zéro overhead réseau : le ciphertext ML-KEM n'est envoyé que toutes les 10 messages (pas à chaque message)
  • Compteur persistant : pqRatchetCounter dans RatchetState (Room), survit aux redémarrages

ChaCha20-Poly1305 — Chiffrement alternatif (V3.5)

Fialka sélectionne automatiquement le chiffrement symétrique optimal selon le hardware :

Hardware Chiffrement Raison
ARMv8 avec Crypto Extension (API 33+) AES-256-GCM Accélération matérielle disponible
Sans accélération AES ChaCha20-Poly1305 Plus rapide en logiciel pur

Détection

hasHardwareAes() :
  → Initialise AES-GCM avec une clé de test
  → Si l'init prend < 1ms → hardware AES présent → AES-256-GCM
  → Sinon → ChaCha20-Poly1305 (BouncyCastle)

Format réseau

Le champ cipherSuite dans le message indique l'algorithme utilisé :

  • 0 (ou absent) = AES-256-GCM (défaut, rétrocompatible)
  • 1 = ChaCha20-Poly1305

Le receiver déchiffre automatiquement avec le bon algorithme.

Propriétés

  • Sélection transparente : l'utilisateur ne choisit pas — le hardware dicte
  • Rétrocompatibilité : les anciens messages (sans cipherSuite) sont déchiffrés en AES-GCM
  • Même sécurité : AES-256-GCM et ChaCha20-Poly1305 offrent tous deux une sécurité 256-bit AEAD

Ce qui transite sur le réseau (Tor P2P)

Messages (chiffrés)

{
  "ciphertext": "a3F4bWx...",
  "iv": "dG9rZW4...",
  "createdAt": 1700000000000,
  "senderHash": "HMAC-SHA256(accountId, conversationId)",
  "signature": "Ed25519(ciphertext || conversationId || createdAt)"
}
  • senderHash = HMAC-SHA256(accountId, conversationId) → l'ID brut n'est plus visible
  • Le message paddé (cf. section Padding) est chiffré → taille uniforme sur le réseau
  • signature = Ed25519 sur ciphertext_UTF8 || conversationId_UTF8 || createdAt_bigEndian8bytes → anti-falsification + anti-replay
  • createdAt = System.currentTimeMillis() côté client pour cohérence signature
  • Tous les messages transitent via Tor Hidden Services (.onion vers .onion) — zéro relais central

Paramètres éphémères

La durée éphémère est synchronisée directement entre pairs via le canal Tor chiffré.

Événements fingerprint

  • Notification événementielle uniquement — ne synchronise pas l'état de vérification
  • Chaque utilisateur gère son état fingerprintVerified localement en Room

Supprimé du wire format (V1.1 metadata hardening)

  • senderPublicKey — inutile en 1-to-1 (le destinataire connaît déjà la clé du contact)
  • messageIndex — chiffré dans le payload AES-GCM (trial decryption côté réception)

Jamais envoyé : texte en clair, clés privées, clés de chaîne, position du ratchet.

Chiffrement de fichiers

Envoi :
  fichier → clé AES-256-GCM aléatoire (fileKey)
  → chiffre fichier (encryptFile) → envoie cipherBytes P2P via Tor
  → message = "FILE|" + fileId + "|" + Base64(fileKey) + "|" + fileName
  → chiffre message avec Double Ratchet → envoie via Tor

Réception :
  → déchiffre message → détecte préfixe "FILE|"
  → reçoit fichier chiffré via Tor P2P
  → déchiffre avec fileKey → sauvegarde stockage interne

Demande de contact (via Tor / Mailbox)

{
  "senderPublicKey": "Ed25519_base64...",
  "senderDisplayName": "Alice",
  "conversationId": "conv_abc123",
  "createdAt": 1700000000000
}

Les demandes de contact sont délivrées directement au service .onion du destinataire, ou stockées dans un Fialka Mailbox si le destinataire est hors-ligne.


Modèle de menace

Menace Protégé ? Détail
Serveur lit les messages Aucun serveur central — tout P2P via Tor Hidden Services, chiffré E2E
Compromission serveur central N/A Aucun serveur central n'existe
Compromission d'une clé message PFS — chaque message a sa propre clé
Replay d'anciens messages messageIndex intégré dans le ciphertext, état par conversation
Race conditions ratchet Mutex par conversation (thread-safe)
Attaque MITM Fingerprint emojis 96-bit (vérification visuelle) + handshake ML-DSA-44
Vol du téléphone déverrouillé Keystore, SQLCipher, App Lock PIN + biométrie, auto-lock
Messages sensibles oubliés Messages éphémères (timer sur envoi / lecture)
Falsification de message Signature Ed25519 par message — badge ✅/⚠️
Métadonnées (qui/quand) Tor Hidden Services (sealed sender), padding uniforme, dummy traffic
Analyse de trafic Dummy traffic (30-90 s, même pipeline), padding par buckets, couverture Tor
Fichiers interceptés Chiffrement AES-256-GCM par fichier, clé dans canal E2E, P2P via Tor
Perte du téléphone Phrase mnémonique 24 mots (BIP-39) pour restaurer l'identité entière
Contact supprime son compte Détection automatique conversation morte + nettoyage + re-invitation
Ordinateur quantique (futur) PQXDH hybride ML-KEM-1024 + SPQR ré-encapsulation + handshake ML-DSA-44
Désynchronisation ratchet Sync à l'acceptation, mutex par conversation
Perf sans accélération AES ChaCha20-Poly1305 sélectionné automatiquement sans ARMv8 Crypto Extension
Screenshot / enregistrement écran FLAG_SECURE sur toutes les fenêtres et dialogs sensibles
Tapjacking / attaque overlay filterTouchesWhenObscured sur les activités sensibles
Injection deep link Whitelist paramètres, limites de taille, validation Base64, rejet caractères de contrôle
Fuite presse-papiers EXTRA_IS_SENSITIVE + auto-effacement 30s
Récupération forensique de fichiers SecureFileManager écrasement 2 passes (aléatoire + zéros) avant suppression
Exposition adresse IP Tout le trafic via Tor — IP réelle jamais exposée aux pairs ou au réseau
Métadonnées push notification UnifiedPush + ntfy.sh — zéro contenu de message, auto-hébergeable

Voir aussi SECURITY.md pour l'analyse complète des mesures de sécurité.