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)?
sha512(sha512(sha512(phonenumber + salt) + phonenumber + salt) + phonenumber + salt)
.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
?
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.
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 bcrypt
pour 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 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.
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.
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.
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.
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.
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:
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.
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.
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.
Cette méthode doit avoir les propriétés suivantes:
Étapes suggérées:
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)S
est utilisé pour saler les hachages de filtre Bloom par une méthode appropriéeA
et B
stockent leurs numéros de téléphone dans un filtre Bloom et les soumettent au serveurA
et B
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.
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.