web-dev-qa-db-fra.com

Trouvez les numéros de téléphone correspondants sans les connaître

Nous avons une application mobile qui, compte tenu de deux utilisateurs, doit leur permettre de voir quels contacts communs ils ont en fonction de leurs numéros de téléphone. Comment pouvons-nous le faire de manière cryptographiquement sécurisée et en respectant la confidentialité des utilisateurs (c'est-à-dire sans partager les numéros en texte brut entre eux ou avec un serveur)?


Notre solution actuelle est:

  1. (Sur le téléphone A) Générez un seul sel aléatoire.
  2. (Sur le téléphone A) Prenez tous les numéros de téléphone du téléphone A et générez un hachage SHA512 avec plusieurs tours pour chaque numéro de téléphone en utilisant le sel généré à l'étape 1. Quelque chose comme sha512(sha512(sha512(phonenumber + salt) + phonenumber + salt) + phonenumber + salt).
  3. (Sur le téléphone A) Envoyez ces hachages avec le sel au téléphone B (via un serveur).
  4. (Sur le téléphone B) Répétez l'étape 2 pour générer des hachages pour ses propres numéros de téléphone en utilisant le même sel généré sur le téléphone A.
  5. (Sur le téléphone B) Comparez les deux listes de hachages - un hachage correspondant signifie un numéro de téléphone correspondant, et donc un contact commun.

Est-ce une approche imparfaite, sujette aux attaques de tables arc-en-ciel/force brute, et dans l'affirmative, existe-t-il une autre solution plus appropriée? Il est peut-être préférable d'utiliser bcrypt avec un sel donné que de faire plusieurs tours de sha512?

39
liviucmg

bcrypt serait une approche un peu meilleure car elle est conçue pour être (programmable) lente.

En utilisant un sel suffisamment grand et un facteur de complexité raisonnable, bcrypt(salt + number, complexityFactor) devrait produire un hachage viable et vous évitez de "rouler votre propre cryptographie", ce qui pourrait s'avérer être une vente difficile. Pour augmenter la sécurité, il vous suffit de lancer complexityFactor.

Un attaquant devrait maintenant générer la bcrypt non seulement de chaque numéro de téléphone à 10 chiffres (ce qui pourrait être faisable: il n'y en a que 10dix chiffres après tout), mais de toutes les séquences salées possibles. Avec un sel base64 à 10 caractères (60 bits d'entropie), la complexité augmente de vingt ordres de grandeur.

Forçage brutal

Supposons que vous ayez 1 000 contacts. Le CPU de votre téléphone moyen semble être deux ordres de grandeur plus lent qu'un tableau de base de serveur. Je pense qu'il est raisonnable de dire que ce sera trois ordres de grandeur plus lent qu'une implémentation GPU semi-dédiée de bcrypt, qui - ne serait pas si efficace .

Nous réglons donc bcryptpour prendre 100 millisecondes pour chaque encodage. Cela signifie que nous avons besoin de 1 minute 40 secondes pour générer nos 1000 hachages, ce qui est un temps raisonnable pour mille contacts (une barre de progression semble en ordre). Si l'utilisateur n'a que 100 contacts, il le fera en 10 secondes.

L'attaquant, étant donné le sel d'un nombre, doit générer peut-être 108 les nombres pour couvrir raisonnablement l'espace du numéro mobile (le premier numéro, et peut-être les deux premiers, ne sont pas vraiment 10 ou 100 - je les compte comme 1). Cela prendra trois ordres de grandeur moins que 108 fois 100 millisecondes, soit 107 secondes. C'est jusqu'à 104 secondes, ou environ deux heures et demie (ou une journée entière si l'optimisation du GPU s'avère ne pas fonctionner).

En moins de quatre mois, les 1 000 contacts au total auront été décryptés - à l'aide d'un serveur optimisé. Utilisez dix de tels serveurs, et l'attaquant aura terminé dans deux semaines.

Le problème, comme souligné par la réponse d'Ángel et les commentaires de Neil Smithline, est que l'espace clé est petit.

En pratique, l'utilisateur A produira quelque chose (un bloc de hachage, ou autre) pour être mis à la disposition de B. d'une manière ou d'une autre. L'utilisateur B doit avoir une méthode qui fonctionne comme

matches = (boolean|NumberArray) function(SomethingFromA, NumberFromB)

(peu de changements si le deuxième paramètre est un ensemble de N nombres, puisque UserB peut construire un ensemble en utilisant un vrai nombre et N-1 nombres connus pour être faux ou non intéressants. Cela peut allonger le temps d'attaque d'un facteur N).

Cette fonction fonctionne dans un temps T ... en fait cette fonction doit fonctionner dans un temps T assez court pour que l'utilisateur B, dans une application commerciale du monde réel, est satisfait.

Par conséquent, une limite que nous ne pouvons pas facilement esquiver est que les nombres M doivent être vérifiés dans un délai acceptable sur un smartphone moyen . Une autre limite que nous ne pouvons pas raisonnablement esquiver est que l'utilisateur B peut fournir de faux numéros à l'algorithme (c'est-à-dire des personnes qui ne sont pas vraiment des contacts, et qui n'existent peut-être même pas).

Les deux limites sont également appliquées si le vérificateur se trouve sur un troisième serveur; cela garantit seulement un délai d'exécution inférieur qui peut contrecarrer certains scénarios, tels que "décrypter tous les numéros de UserA", mais pas d'autres tels que "vérifier qui a ce nombre ", comme dans réponse de drewbenn ).

De ces deux limites découle le fait que l'utilisation d'un smartphone (ou d'un serveur tiers avec un temps d'exécution minimal), parcourir les 108 un nombre raisonnable prend environ 108 smartphoneTime time, ou de l'ordre de mille mois.

Les stratégies d'attaque pour réduire ce temps sont de distribuer l'attaque entre plusieurs vérificateurs, ou de l'exécuter sur un serveur non limité et plus rapide (cela nécessite que l'algorithme soit disponible, mais en supposant que le contraire est la sécurité par l'obscurité), et ils semblent à la fois faisables et abordables .

Une échappatoire?

Une possibilité pourrait être d'introduire une faible probabilité de faux positifs . C'est-à-dire que la fonction Oracle ci-dessus va occasionnellement (disons une fois tous les dix mille contacts), et de manière déterministe sur l'entrée de UserA, retourner vrai à l'un des numéros de UserB.

Cela signifie que l'attaque par force brute sur 108 les chiffres donneront les contacts de UserA mêlés à 104 d'autres numéros. Le déterminisme sur l'entrée de UserA signifie que deux contrôles successifs sur ces 104 les objets trouvés ne les affaibliront pas davantage. À moins que UserB ne puisse récupérer une copie différente de l'entrée de UserA, ce qui produira un ensemble différent de faux positifs et permettra de filtrer les vrais contacts comme l'intersection des deux ensembles, cela peut rendre la réponse brute forcée moins attrayante. Cela a un coût - les utilisateurs honnêtes devront obtenir le faux coup occasionnel.

Nous ne pouvons vraiment pas gagner

Si UserB doit pouvoir répondre à la question "Le numéro X est-il parmi les contacts de UserA?" dans un temps raisonnable avec certitude, la dépense de temps est linéaire , car le système ne peut pas empêcher que deux de ces demandes soient faites contre les nombres X1 et X2, et le temps pour la demande X2 sera la même pour la demande X1. Par conséquent, la résolution de deux nombres nécessitera le double de ce délai raisonnable; par induction, la résolution de N nombres impliquera N fois ce temps raisonnable ( pas, disons, N2).

La différence entre une requête légitime et une attaque est que l'attaque fonctionnera sur un espace dix à cent mille fois plus grand. Étant linéaire, il nécessitera un temps jusqu'à cent mille fois plus long ... mais il peut également fonctionner sur une machine ou un groupe de machines cent à mille fois plus rapidement.

Par conséquent, notre attaquant sera toujours capable de décrypter tous les contacts de UserA en un temps "toujours pas déraisonnable". La seule vérification sérieuse serait que les contrôles soient exécutés sur une troisième machine de confiance avec limitation de débit et les moyens de détecter une attaque probable.

Pour contrecarrer un attaquant, nous avons besoin de quelque chose de mauvais pour augmenter avec l'augmentation de N, et comme il ne peut pas être temps d'exécution (qui n'augmente pas assez), je pense que le seul recours restant est la probabilité de faux positifs. L'attaquant obtiendra toujours la réponse, mais nous pourrions toujours réussir à rendre une réponse brute forcée moins utilisable.

Une implémentation simpliste (filtre Bloom du pauvre)

Pour répondre au commentaire de Mindwin, l'algorithme local ne peut pas fonctionner en cachant des informations - les informations doivent être manquantes en premier lieu, sinon nous ferions toujours de la sécurité par l'obscurité.

Une méthode serait que UserA (Alice) envoie le bcrypt sel pour elle (disons) 1000 contacts, suivi de 1000 incomplet bcrypt hachages. Si les hachages sont tronqués au i-ème octet, il y aura des collisions pseudo-aléatoires. Parmi les contacts de UserB (Bob), qui sont peu nombreux, les collisions seront très rares (sauf si i est vraiment petit). Parmi l'espace des nombres entiers de l'attaquant (Eve), les collisions seront importantes.

Notez que la distribution des numéros de téléphone n'est pas plate, donc Eve peut avoir des moyens de réduire ces collisions en supprimant, disons, les séquences de numérotation inutilisées.

Si chaque hachage de contact a une probabilité de collision de un sur mille, Bob, vérifiant ses mille contacts, a une probabilité de (1 - 1/1000)1000 de ne pas avoir de collisions du tout - c'est 70%, pas si bon. Si la probabilité de collision est de 1/10000, Bob avec mille contacts aura 90% de chances de ne pas avoir une seule collision. Sur une centaine de contacts uniquement, les probabilités de non-coll pour Bob sont respectivement de 90% et 99%.

Eve, vérifiant 108 les nombres, même avec p = 1/10000, obtiendront toujours dix mille collisions, quoi qu'il arrive.

L'envoi de deux hachages ou plus avec une probabilité de collision plus élevée ne change pas grand-chose pour Bob ou Eve, par rapport à l'envoi d'un hachage unique avec une probabilité de collision égale au produit des hachages séparés.

Par exemple, au lieu d'un tour avec p = 1/10000, utilisez deux tours avec p = 1/100, car 1/100 * 1/100 = 1/10000.

Alice envoie donc deux ensembles de hachages incomplets non ordonnés, avec des graines différentes, et une probabilité de collision plus élevée de 1%; Bob testera ses 1000 contacts et obtiendra des correspondances positives pour les 100 contacts qu'il a en commun; les 900 restants ne devraient pas correspondre, mais comme le hachage est incomplet, 1% d'entre eux le feront, ce qui signifie 9 contacts parasites, et Bob se retrouvera avec 109 candidats probables après avoir effectué 1000 tests. Il doit maintenant tester ces 109 avec le deuxième hachage, qui a également une probabilité de 1%. Les 100 véritables intersections correspondront toujours. Sur les 9 restants, aucun ne passera probablement. La chance qu'un contact passe deux de ces tours est de 1% sur 1%, soit 1 sur 10000, et la chance d'avoir pas même un faux positif sur 1000 contacts non correspondants est (1-1/10000)1000, soit 90,48%, exactement comme avant.

Avec les mêmes chiffres, Eve obtiendra un million de faux positifs lors de son premier tour et devra effectuer un million de tests supplémentaires. 1% de ceux-ci correspondront au deuxième tour, laissant Eve avec dix mille faux positifs mélangés avec les mille contacts d'Alice.

Eve a dû exécuter 101 millions de tests au lieu de 100, et Bob a dû exécuter 1109 tests au lieu de 1000. En proportion, le schéma de double hachage affecte Bob plus durement qu'Eve. Il serait préférable d'utiliser un seul hachage avec une complexité plus élevée.

Le problème de confidentialité de la réponse à la question "Alice connaît-elle le numéro N?" restera sans réponse - le temps de répondre est le même pour Bob et Eve.

26
LSerni

Il y a potentiellement d'autres problèmes de confidentialité que vous ne considérez pas encore. De par sa conception, votre application permet de voir facilement qui est connecté à une certaine cible. Ainsi, un attaquant crée un contact sur son téléphone (l'activiste/informateur/terroriste/victime qui l'intéresse), puis se connecte à de nombreux autres utilisateurs via votre application, pour créer une liste des contacts de la cible. Ainsi, par exemple, un abuseur de DV pourrait utiliser cette application pour dresser une liste de personnes toujours en contact avec son ex: même Google a eu des problèmes pour obtenir ce droit.

17
user15392

Oui, c'est (un peu) imparfait. Le problème est que l'espace est trop petit, donc même avec les multiples rounds et sels, il est relativement facile de forcer brutalement.

Open Whisper Systems avait un système plein d'esprit où ils fournissaient un filtre de chiffrement chiffré pouvant être interrogé localement à l'aide de signatures aveugles. Ils expliquent le processus (ainsi que fournissent une bonne discussion sur les problèmes de récupération d'informations privées) à https://whispersystems.org/blog/contact-discovery/

Malheureusement, ils ont dû arrêter cela sur TextSecure en raison de problèmes pratiques avec une grande base d'utilisateurs. Dans votre cas, comme vous partagez des nombres entre deux utilisateurs finaux, cela devrait être faisable, soit avec leur même méthode, soit en utilisant un autre protocole comme ceux publiés qui sont mentionnés par Moxie.

11
Ángel

Comment pouvons-nous le faire de manière cryptographiquement sécurisée et en respectant la confidentialité des utilisateurs (c'est-à-dire sans partager les numéros en texte brut entre eux ou avec un serveur)?

tldr: Vous ne pouvez pas.

Le hachage est idéal pour certaines utilisations, mais ce n'est probablement pas l'un d'entre eux. La raison en est qu'un attaquant saurait qu'il n'y a que 10 milliards de possibilités (pour les numéros de téléphone à 10 chiffres), ce qui rend trop facile de forcer brutalement les hachages découverts.

Au lieu de cela, vous pouvez accomplir ce que vous voulez si:

  • l'un des téléphones est prêt à faire confiance à l'autre. Un téléphone partage sa liste de contacts avec l'autre, et l'autre fait la comparaison. Le deuxième téléphone n'a pas besoin de partager sa liste avec le premier.
  • vous êtes prêt à utiliser un médiateur de confiance, c'est-à-dire un serveur. C'est la meilleure approche du point de vue des utilisateurs car aucune des parties n'a besoin de partager sa liste de contacts avec l'autre. Les deux partageraient leurs listes avec le médiateur et il rendrait compte des matches, et promet de ne rien sauver. La communication avec chaque partie impliquée utiliserait des paires de clés de cryptage publiques/privées standard, et les données de personne ne seraient pas en danger. Bien sûr, cela suppose que vos utilisateurs vous feront confiance lorsque vous vous engagez à ne pas conserver leurs listes de contacts. Mais si vous pouvez faire en sorte que vos utilisateurs vous fassent suffisamment confiance pour installer votre application, vous avez déjà gagné leur confiance.
7
TTT

Faisons quelques tests!

J'ai commencé avec une implémentation bash naïve, et calculé 10k nombres en 33 secondes:

#!/bin/bash

phone="2125551212"
salt="abcdefghijklmnopqrstuvwxyz"

shasalt() { echo "$* $phone $salt" | sha512sum; }

for f in {1..10000}
do
    shasalt $(shasalt $(shasalt)) >/dev/null # or write to a file...
    ((phone++))
done
echo $phone>&2

Puis en utilisant un SHA512 algorithme C++ que j'ai trouvé en ligne J'ai écrit un exemple de code C++ laid. J'ai généré des hachages pour un code de zone entier en 84 secondes, ou environ 160 secondes lorsque je les ai écrites dans un fichier (en générant une table Rainbow de 1,4 Go):

#include "sha512.hh"
#include <iostream>
#include <sstream>
#include <string.h>
using namespace std;

int main(int argc, const char* argv[])
{
  char salt[10] = "liviucmg";
  int x = 2120000000; // won't work above 2^31 i.e. past area code 213
  char y[21];
  snprintf(y, 21, "%010d,%s", x, salt);
  const int len = strnlen(y, 21);

  for (int i = 0; i < 10000000; i++, x++) {
      snprintf(y, 21, "%010d,%s", x, salt);
      //cout << y << "," << sw::sha512::calculate(y, len) << endl;
      string FIRST(sw::sha512::calculate(y, len));
      string FIRST_GO(FIRST + y);
      string SECOND(sw::sha512::calculate(&FIRST_GO, 1+FIRST_GO.length()));
      string SECOND_GO(SECOND + y);
      // uncomment this line to generate a hash table:
      //cout << y << "," << sw::sha512::calculate(&SECOND_GO, 1+SECOND_GO.length()) << endl;
  }
  return 0;
}

Tout cela était sur mon ordinateur portable de 3 ans, qui peut être trouvé en ligne dans une configuration similaire pour environ 300 $. Ainsi, pour une dépense assez minime, un attaquant pourrait forcer n'importe quel nombre à 10 chiffres et combinaison unique de sel en environ une journée (moins s'ils ne testaient que les codes de zone réels), ou tous les numéros intéressants (ceux de l'indicatif régional de la cible et des indicatifs physiques adjacents) dans environ 5 minutes. Je n'ai pas essayé de régler (ou de déboguer!) Mon code de test, donc j'imagine que ces temps pourraient être réduits de moitié en réglant le code. Le faire fonctionner sur un meilleur matériel, comme une carte graphique ou un FPGA, réduirait également considérablement le temps: je suppose que n'importe quel nombre + sel pourrait être forcé brutalement en environ une heure ou moins par n'importe quel attaquant compétent.

5
user15392

Avec mention Isemis de "probabilité de faux positifs", j'ai pensé à preuve de connaissance zéro . Cette réponse ne prétend pas être sécurisée car elle n'a jamais été examinée, les autres devraient donc l'examiner et la commenter. Je ne suis pas non plus un expert en sécurité professionnel et je n'ai pas eu le temps de m'assurer que le faible nombre de numéros de téléphone possibles pourrait être un problème.

  1. L'utilisateur A et l'utilisateur B se connectent (directement ou via le serveur) et attribuent un identifiant aléatoire (RI) à chaque contact leurs contacts et le stocke dans une liste (LA et LB) avec le numéro de téléphone rempli avec le nonce et après l'application a fonction de dérivation de clé . LA reste à A et LB reste à B.
  2. L'utilisateur A prouve une série de connaissances (voir Zero-knowledge proof ) pour chacun des nombres de la liste LA avec RI. L'utilisateur B doit découvrir par bruteforce à quels contacts dans LB chaque RI de A pourrait correspondre. Chaque contact sans ajustement est supprimé de LB. Les ajustements possibles sont stockés dans la liste pour le prochain tour afin d'améliorer la vitesse de bruteforce.
  3. L'utilisateur B prouve une série de connaissances pour chacun des numéros de la liste LB avec RI. L'utilisateur A doit découvrir par bruteforce à quels contacts dans LA chaque RI de B pourrait correspondre. Chaque contact sans ajustement est supprimé de LA. Les ajustements possibles sont stockés dans la liste pour le prochain tour afin d'améliorer la vitesse de bruteforce.
  4. Répétez les étapes 2 et 3 assez souvent pour être statistiquement sûr que chacun a prouvé chacun des numéros à l'autre utilisateur.
  5. Facultatif: échangez les restes dans les listes LA et LB de manière sécurisée mentionnée par d'autres ou via un canal sécurisé entre l'utilisateur A et l'utilisateur B pour être sûr que la correspondance des IR était correcte.

Avec cet algorithme, le serveur ou un observateur n'apprend jamais aucune information pertinente sur les contacts de A ou B, sauf leur nombre et le nombre qu'ils ont en commun. Les utilisateurs A et B n'apprennent que les mêmes informations que le serveur et les contacts communs.

Étant donné que les premières étapes de l'algorithme sont exponentielles et gourmandes en ressources, il convient d'inclure une protection contre DOS dans les implémentations.

3
H. Idden

Cette méthode doit avoir les propriétés suivantes:

  • La probabilité de faux positifs peut être aussi faible que souhaité
  • Le serveur apprend uniquement le nombre (approximatif) d'éléments de l'annuaire téléphonique de chaque personne, mais pas les nombres eux-mêmes et ne peut pas les forcer brutalement
  • Les attaques par force brute côté client sont impossibles, car le serveur peut appliquer des stratégies à leur encontre
  • Les téléphones n'apprennent pas le nombre de contacts d'un autre téléphone

Étapes suggérées:

  1. Les téléphones A et B génèrent un secret partagé S en utilisant un protocole d'accord de clé (l'authentification serait bien ici pour se défendre contre MitM)
  2. La taille du filtre Bloom et le nombre de hachages peuvent être codés en dur, le protocole pourrait imposer qu'au maximum 10 000 numéros de téléphone sont autorisés/personne (pour atténuer les collisions et les abus de hachage)
  3. S est utilisé pour saler les hachages de filtre Bloom par une méthode appropriée
  4. A et B stockent leurs numéros de téléphone dans un filtre Bloom et les soumettent au serveur
  5. Le serveur confirme que la cardinalité estimée des filtres n'est pas trop élevée, personne ne peut prétendre connaître tous les nombres possibles
  6. Le serveur calcule l'intersection des ensembles et envoie le résultat à A et B
  7. Maintenant, A et B savent (assez précisément) quels nombres ils ont en commun en vérifiant quels nombres sont allés aux bacs intersectés.

En fait, le filtre Bloom pourrait être remplacé en soumettant des hachages SHA-256 simples au serveur et des hachages correspondants peuvent être soumis aux clients. Cela simplifie beaucoup la logique et a moins de risques de faux positifs.

1
NikoNyrh

Pourquoi les hacher? Pourquoi ne pas chiffrer à la place et les envoyer à votre propre serveur. De cette façon, aucun appareil client ne doit avoir accès aux contacts de l'autre et le serveur fait la plupart du travail.

Cela n'introduit cependant qu'un seul point de défaillance, le serveur. S'il était compromis, les attaquants pourraient potentiellement accéder à tous les numéros. Cela pourrait être contrôlé si rien n'est vraiment stocké et que tout est fait en mémoire.

0
Awn