La démocratisation du séquençage du génome humain, ouvrant les portes de la médecine personnalisée, provoque aussi beaucoup d'inquiétudes au sujet de la protection des données. La séquence unique de l'ADN d'un individu, en effet, peut indiquer entre autres les prédispositions à des maladies, la tolérance à diverses substances, les traits potentiels de la descendance, le phénotype de l'individu, son origine, etc.
Dans les "biobanques", des bases de données comprenant le code génétique de nombreux volontaires sont en cours de construction et ne feront que grandir. A titre d'exemple, le CHUV de Lausanne, précurseur européen dans le domaine, possède actuellement dans les 15'000 échantillons de sang de patients volontaires et en prévoit 50'000, majoritairement destinés au séquençage, d'ici 2017. Naturellement, les administrateurs de biobanques sont conscients que la sécurité de l'information est une question capitale dont il faut se préoccuper — techniquement et légalement — avant de commencer à séquencer en masse.
Car qu'arriverait-il si des gens mal intentionnés tombaient par un heureux hasard sur une base de données reliant l'identité d'un grand nombre de personnes à leur génome ? Sans même parler d'utilisations criminelles, on pourrait par exemple les vendre pour une fortune à des assurances-maladie qui pourraient choisir leurs clients ou adapter leurs primes. Par ailleurs, même si la séquence est anonyme, on peut imaginer dans le futur pouvoir retrouver à qui elle appartient simplement en la comparant au phénotype de la personne.
On a donc tout intérêt à protéger au mieux les données génétiques. Heureusement, du coté informatique, il y a du progrès. Même si la méthode que je vais vous présenter ici n'a rien de spécifique à la biologie, elle offre une opportunité intéressante de résoudre le problème.
L'encryption homomorphique
Le schéma de cryptage le plus répandu actuellement est le système clé privée-clé publique RSA. Très rapide, il sera pourtant caduc aussitôt qu'un véritable ordinateur quantique sera disponible. De plus, crypter la base de données c'est bien, mais à terme on veut utiliser ces données pour les analyser. Il faut donc décrypter l'information, faire le calcul (sur des machines pas forcément sûres), puis ré-encrypter le résultat, ce qui signifie qu'à un moment donné, l'information non cryptée est lisible, résultat de l'analyse compris.
Une solution à ce problème porte le nom d'"encryption homomorphique", une méthode rendue possible en théorie par un certain Craig Gentry et qui a été améliorée depuis au point d'être utilisable en pratique. Ce que cette méthode a de génial, c'est qu'on peut effectuer les calculs directement sur les données encryptées, sans jamais les décoder ! Pour schématiser, on peut encoder le chiffre 3 en "asdf" et 4 en "qwer" dans la base de données, puis sur un serveur distant calculer "asdf * qwer" qui donne "djfh76gz" en retour, et ce dernier une fois décrypté vaudra… 12. Magique.
Fonctionnement
Je base mes explication sur cet excellent billet, en essayant toutefois de rester moins technique. Pour coder un bit ,
- On génère une clé binaire de longueur choisie : un vecteur aléatoire rempli de 0 et 1.
- On cherche un vecteur réel tel que . Notez le sous le signe d'égalité, on y reviendra. La multiplication est un produit scalaire.
, un vecteur de longueur aussi, est alors une version cryptée de B. On crypte un message complet en cryptant chaque bit de chaque caractère (en principe 1 caractère ASCII = 7 bits).
Pour décrypter c'est tout simple : si on connaît et , on n'a qu'à les multiplier (mod 2) et arrondir le résultat.
Si on y réfléchit, tel que je l'ai présenté là le système n'est pas sûr du tout : sans connaître la clé, il suffit d'encoder fois le bit 1, résoudre le système de équations à inconnues qui en résulte, et on trouve la clé…
C'est là que le entre en jeu. Si on n'a pas mais presque : , alors les algorithmes permettant de résoudre les systèmes linéaires étant instables numériquement, les erreurs s'accumulent rapidement et il est impossible de retrouver la clé de cette manière. On va donc volontairement ajouter un peu de "bruit" à notre vecteur , de sorte qu'en connaissant la clé on retrouve toujours la même réponse (il suffit d'arrondir le résultat du produit scalaire), mais qu'il devienne impossible de deviner la clé.
Preuve que ça marche
Ce qu'il est important de montrer maintenant, c'est pourquoi on peut calculer directement sur les valeurs cryptées. En fait, dans un ordinateur et sur des binaires, il n'y a pas une infinité de fonctions pour lesquelles il faut vérifier cette propriété : toutes les opérations de base (shift, addition, soustraction, multiplication, division) dérivent de l'addition binaire.
Justement, "homomorphique", même s'il sonne comme un terme compliqué, signifie simplement "linéaire". Au sens mathématique, ça signifie principalement qu'appliquer la fonction à deux éléments et les additionner, c'est la même chose que les additionner d'abord et appliquer la fonction ensuite. Ici on va montrer que le schéma d'encryption est linéaire pour l'addition binaire.
Une addition de deux nombres binaires peut être réalisée à l'aide des deux opérations "par bit" XOR et AND. On peut vérifier que le XOR de deux bits s'obtient en les additionnant (mod 2), et le AND en les multipliant.
Il suffit donc de montrer que pour ces deux opérations, l'appliquer avant ou après cryptage donne un résultat identique. Voilà la démonstration pour le XOR — celle pour le AND prend un peu plus de place (voir ici, au paragraphe "Multiplication is a bit more tricky").
Prenons deux bits et qu'on encode dans des vecteurs et . Alors
Donc
ou
,ce qui montre que , une fois décodé, donne le résultat de .
Autrement dit, on peut faire le calcul directement sur les valeurs cryptées et obtenir le même résultat.
Sécurité
On travaille toujours avec les données sous leur forme cryptée, soit. Mais il faut encore qu'elles ne soient pas faciles à décrypter. On a montré qu'ajouter un peu de "bruit" ne permettait pas une résolution triviale, ce qui ne signifie pas encore que le schéma est sûr.
Heureusement, on peut montrer que la résolution est un problème classé NP-difficile (NP-hard : dont la résolution est impossible en un temps polynomial). De plus, contrairement à quelques-uns de ces problèmes dont certaines configurations initiales sont simples à résoudre et d'autres très difficiles, il est difficile dans le cas moyen (average-case hard). Tout cela le rend très probablement imperméable à des adaptations connues d'algorithmes quantiques tels que celui de Shor qui menace bientôt vos clés RSA.
La cause, c'est que le problème est équivalent à une recherche du plus petit vecteur d'un réseau. Un réseau, au sens mathématique, c'est l'espace généré par tous les multiples entiers des vecteurs de base. Par exemple, l'ensemble des nombres entiers est un réseau à 1 dimension (on l'appelle Z), ainsi que l'ensemble des entiers pairs (2Z) ou des multiples de 17 (17Z). Il est plus commun de nommer 2 comme générateur de 2Z, mais on pourrait aussi choisir 9 et 29, puisque . A partir de 9 et 29, il est déjà plus difficile de déterminer que le plus petit vecteur est 2. Maintenant imaginez le même problème en deux dimensions (un "grillage"), où on doit en gros trouver la taille des "mailles" à partir de seulement quelques points quelconques du grillage, puis en N dimensions.
Il est aussi possible de vérifier que les données ont été traitées par la bonne fonction et que le résultat retourné est bien celui attendu (pas truqué ou intercepté en route par un tiers malveillant), ce qui est nécessaire en pratique pour avoir un protocole utilisable. Enfin, le schéma s'étend naturellement à un protocole à clé publique, comme le RSA actuel.
Limitations
Malheureusement, c'est encore assez lent. De nombreux travaux mathématiques ont déjà amélioré le temps de calcul de "âge de l'Univers" à "quelques minutes", ce qui est très bien. Aux dernières nouvelles (essais au CHUV de Lausanne), il fallait 127ms pour encrypter l'information d'une variante (SNP) du génome humain. Faites le calcul pour encrypter toutes les variantes d'un seul individu : … environ 15 jours. Et il y en a 100'000 à traiter comme ça.
Je veux essayer !
Le principe a l'air très simple. Mais pour avoir essayé de programmer moi-même quelques fonctions qui montrent le fonctionnement, je peux vous dire que ce n'est pas si évident à faire proprement. Pour une véritable librairie, voir par exemple HElib, qui d'ailleurs affiche plus de 30'000 lignes de C++.
Voici tout de même quelques bouts de code (en Julia, mais c'est complètement arbitraire) qui auront au moins le mérite de montrer à quoi ressemblent des données une fois cryptées, et d'illustrer un peu le sujet. A vous de l'améliorer/corriger/compléter/oublier autant que vous le souhaitez.
On va encoder, au hasard, le nombre 23 (sous sa forme binaire) :
julia> message = bin(23)
"10111"
Tout d'abord, on génère une clé binaire aléatoire K :function generate_key(N=20)
key = rand(0:1, N)
while countnz(key) == 0 # on ne veut pas une cle avec 0 partout
key = rand(0:1, N)
end
return key
endjulia> K = generate_key(20)
20-element Array{Int64,1}:
0 0 1 1 0 1 1 0 1 0 0 1 0 1 0 1 0 0 1 0
On commence par savoir encoder 1 bit à la fois grâce à notre clé :function encode_1bit(bit, key)
last1 = findin(key,1)[end] # dernier bit non-zero
c = Float64[] total = 0
noise = 0 # pas de bruit pour le moment !!
for i=1:length(key)
a = rand()*2 - 1
if i == last1
push!(c, key[i]-total-(1-bit)+noise)
else
push!(c, a)
end
if key[i] != 0
total = total + a
end
end
return c
end
Ici le bit 0 :julia> encode_1bit(0, K)
20-element Array{Float64,1}:
-0.841112 0.376839 -0.64088 -0.0220782 0.91517 -0.341114 0.365112 -0.650964 -0.528641 0.00648227 0.146742 0.435683 0.36608 0.822643 -0.361396 -0.259946 -0.694225 0.811952 0.169222 0.398963
Puis on l'utilise pour coder un message entier (ici une chaîne de caractères '0'/'1', comme "10111") :function encode(message::String, key)
message = map(int, split(message,"")) # string -> array de 0/1
encrypted = zeros(length(key), length(message))
for (i,bit) in enumerate(message)
c = encode_1bit(bit, key)
encrypted[:,i] = c
end
return encrypted
endjulia> C = encode(message, K)
20x5 Array{Float64,2}:
-0.348828 0.696748 -0.683862 0.714204 0.982248
-0.875624 0.689617 0.925301 0.0433602 0.0421338
0.700639 -0.592715 -0.340894 -0.452295 -0.0494057
0.504052 -0.169575 -0.144381 0.206843 0.941349
0.431316 -0.928031 -0.0359552 0.118174 0.142373
-0.377039 0.481692 -0.472779 0.800013 0.529427
0.752665 -0.202764 0.432656 -0.587943 -0.975236
-0.623157 -0.701443 0.151177 0.071244 0.789276
0.959001 0.00547738 0.574379 0.604067 -0.929959
-0.838194 -0.93639 -0.131882 -0.565128 -0.839332
-0.387583 0.189935 -0.652823 0.106463 -0.881375
0.586078 -0.602708 0.682506 -0.711591 0.706119
0.730341 0.148296 0.25684 -0.273813 0.860193
-0.310579 0.93636 0.778648 -0.105344 -0.0819126
-0.830075 0.544769 0.962231 0.271662 0.916679
-0.9015 -0.748283 -0.0365485 0.0349416 0.326137
-0.838072 0.976137 -0.0814949 -0.12174 0.072706
-0.848451 -0.164284 -0.0186105 0.84279 -0.104672
-0.913316 0.892514 -0.473587 1.21131 0.53348
-0.547914 0.378009 0.825841 -0.920965 -0.742074
Un simple nombre est donc transformé en une matrice réelle qui grandit avec la taille de la clé et le nombre de bits utilisés pour représenter le message - d'où la lenteur du système.Pour décoder, on multiplie la forme encodée par la clé et on arrondit :
function decode_1bit(encrypted_bit, key)
return abs(round((key' * encrypted_bit)[1] % 2))
endfunction decode(encrypted, key)
M = size(encrypted,2)
decrypted = zeros(Int, M)
for i in 1:M
decrypted[i] = decode_1bit(encrypted[:,i], key)
end
return join(decrypted)
endjulia> decode(C,K)
"10111"
On voit qu'on retrouve la forme binaire de 23.Comme on n'a pas ajouté le terme de bruit, il est facile de trouver la clé en encodant suffisamment de fois le bit 1 :
# Now suppose we don't have the key
# We generate N encryptions of 1 and solve the system
function hack()
K = generate_key(10)
println("Test key:", K)
N = length(K)
encrypt = zeros(N, N)
for i=1:N
encrypt[:,i] = encode_1bit(1, K)
end
O = ones(N)
guess = int(encrypt' \ O)
if guess == K
println("Guessed the key !!! :", K)
else:
println("Failed to guess the key")
end
endjulia> hack()
Test key:[1,1,1,1,0,1,0,0,0,1] Guessed the key !!! :[1,1,1,1,0,1,0,0,0,1]
Maintenant en exercice, vous pouvez vous amuser à programmer, toujours dans l'espace des valeurs cryptées, les fonctions qui correspondent à XOR et AND, puis grâce à elles la fonction d'addition, et finalement comparez le résultat de l'addition de deux entiers, une fois décryptée, avec le résultat connu.Conclusion
Alors l'encryption homomorphique, pour bientôt ? En pratique, pas tout de suite. Si c'est clairement le Graal des cryptologues, il faudrait encore trouver le moyen d'accélérer les calculs. En attendant, les bonnes vieilles clés RSA devraient tenir bon quelques années.
Un autre côté bien frustrant à installer des systèmes aussi sophistiqués, c'est qu'aussi sûre que soit la protection des données personnelles des patients... ceux-ci la publieront librement tout seuls de leur côté. Qu'un site internet qui vous promette de trouver votre âme soeur génétique en échange de votre consentement à partager ces infos, et voilà, les génomes se baladent sur le web.
Quelques références
Un article dans Nature qui a fait du bruit.
Le travail de thèse original de Craig
VenterGentry.Un billet de blog fantastique (en anglais) et sa suite qui ont pas mal inspiré mon article.
Une implémentation en C pour l'encryption homomorphique : HElib.
Merci à Yoann M. et Sylvain P. pour leur relecture
Laisser un commentaire