fhe-cli 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fhe-cli might be problematic. Click here for more details.
- fhe_cli-1.3.1.dist-info/METADATA +280 -0
- fhe_cli-1.3.1.dist-info/RECORD +37 -0
- fhe_cli-1.3.1.dist-info/WHEEL +5 -0
- fhe_cli-1.3.1.dist-info/entry_points.txt +2 -0
- fhe_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
- fhe_cli-1.3.1.dist-info/top_level.txt +1 -0
- fhecli/__init__.py +5 -0
- fhecli/__main__.py +6 -0
- fhecli/cli.py +220 -0
- fhecli/data/info.toml +77 -0
- fhecli/data/originals/ex10_ckks_bonus.py +84 -0
- fhecli/data/originals/ex1_cesar.py +92 -0
- fhecli/data/originals/ex2_xor_malleability.py +102 -0
- fhecli/data/originals/ex3_fernet_tamper.py +92 -0
- fhecli/data/originals/ex4_modpow.py +73 -0
- fhecli/data/originals/ex5_modinv.py +77 -0
- fhecli/data/originals/ex6_paillier_keygen.py +85 -0
- fhecli/data/originals/ex7_paillier_enc_dec.py +118 -0
- fhecli/data/originals/ex8_paillier_homomorphic.py +144 -0
- fhecli/data/originals/ex9_scoring_paillier.py +143 -0
- fhecli/data/solutions/ex10_ckks_bonus.sol +8 -0
- fhecli/data/solutions/ex1_cesar.sol +6 -0
- fhecli/data/solutions/ex2_xor_malleability.sol +7 -0
- fhecli/data/solutions/ex3_fernet_tamper.sol +7 -0
- fhecli/data/solutions/ex4_modpow.sol +5 -0
- fhecli/data/solutions/ex5_modinv.sol +6 -0
- fhecli/data/solutions/ex6_paillier_keygen.sol +8 -0
- fhecli/data/solutions/ex7_paillier_enc_dec.sol +11 -0
- fhecli/data/solutions/ex8_paillier_homomorphic.sol +12 -0
- fhecli/data/solutions/ex9_scoring_paillier.sol +15 -0
- fhecli/info.py +46 -0
- fhecli/resources.py +28 -0
- fhecli/runner.py +72 -0
- fhecli/solutions.py +30 -0
- fhecli/ui.py +70 -0
- fhecli/watch.py +116 -0
- fhecli/workspace.py +58 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex10 (bonus) : votre premier calcul FHE réel avec CKKS (TenSEAL)
|
|
3
|
+
=================================================================
|
|
4
|
+
|
|
5
|
+
Paillier n'additionne que des entiers. Pour un vrai score à base de nombres
|
|
6
|
+
réels (revenus, ratios, poids décimaux), on utilise CKKS, le schéma de la
|
|
7
|
+
bibliothèque TenSEAL (wrapper de Microsoft SEAL). CKKS chiffre des vecteurs de
|
|
8
|
+
réels et sait, sur les chiffrés, additionner, multiplier et faire des produits
|
|
9
|
+
scalaires. C'est le même schéma que la démo web du projet (04_demo).
|
|
10
|
+
|
|
11
|
+
Ce bonus nécessite TenSEAL, non installé par défaut :
|
|
12
|
+
pip install "fhe-cli[ckks]"
|
|
13
|
+
Sans lui, les tests de cet exercice sont simplement ignorés (skipped).
|
|
14
|
+
|
|
15
|
+
make_context() est fourni (mêmes paramètres que la démo). Vous implémentez le
|
|
16
|
+
produit scalaire chiffré.
|
|
17
|
+
|
|
18
|
+
À implémenter :
|
|
19
|
+
- encrypted_dot_product(context, values, weights) -> float
|
|
20
|
+
1. Chiffrer values dans un vecteur CKKS : ts.ckks_vector(context, values)
|
|
21
|
+
2. Produit scalaire avec les poids CLAIRS : enc_vector.dot(weights)
|
|
22
|
+
3. Déchiffrer et renvoyer le premier élément : .decrypt()[0]
|
|
23
|
+
|
|
24
|
+
Ce que ça démontre : le serveur calcule une combinaison pondérée des données
|
|
25
|
+
du client sans jamais les voir en clair. CKKS étant approximatif, le résultat
|
|
26
|
+
est très proche de la valeur attendue, pas exactement égal (d'où le abs(...) <
|
|
27
|
+
1e-3 dans les tests).
|
|
28
|
+
|
|
29
|
+
Testez avec : fhecli run ex10_ckks_bonus
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# I AM NOT DONE
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import tenseal as ts
|
|
36
|
+
except ImportError: # bonus optionnel : pip install "fhe-cli[ckks]"
|
|
37
|
+
ts = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def make_context():
|
|
41
|
+
"""Fourni : contexte CKKS prêt à l'emploi (mêmes paramètres que la démo)."""
|
|
42
|
+
context = ts.context(
|
|
43
|
+
ts.SCHEME_TYPE.CKKS,
|
|
44
|
+
poly_modulus_degree=8192,
|
|
45
|
+
coeff_mod_bit_sizes=[60, 40, 40, 60],
|
|
46
|
+
)
|
|
47
|
+
context.generate_galois_keys()
|
|
48
|
+
context.global_scale = 2 ** 40
|
|
49
|
+
return context
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def encrypted_dot_product(context, values, weights):
|
|
53
|
+
"""Chiffre `values`, calcule le produit scalaire chiffré avec `weights`, déchiffre."""
|
|
54
|
+
# TODO: implémentez les 3 étapes décrites dans l'énoncé
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
import pytest
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_matches_plaintext_dot_product():
|
|
66
|
+
pytest.importorskip("tenseal", reason="bonus CKKS : pip install \"fhe-cli[ckks]\"")
|
|
67
|
+
context = make_context()
|
|
68
|
+
values = [1500.0, 30.0, 1.0]
|
|
69
|
+
weights = [0.005, -0.9, -10.0]
|
|
70
|
+
|
|
71
|
+
result = encrypted_dot_product(context, values, weights)
|
|
72
|
+
expected = sum(v * w for v, w in zip(values, weights))
|
|
73
|
+
assert abs(result - expected) < 1e-3
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_matches_plaintext_dot_product_other_values():
|
|
77
|
+
pytest.importorskip("tenseal", reason="bonus CKKS : pip install \"fhe-cli[ckks]\"")
|
|
78
|
+
context = make_context()
|
|
79
|
+
values = [-1500.0, 55.0, 4.0]
|
|
80
|
+
weights = [0.005, -0.9, -10.0]
|
|
81
|
+
|
|
82
|
+
result = encrypted_dot_product(context, values, weights)
|
|
83
|
+
expected = sum(v * w for v, w in zip(values, weights))
|
|
84
|
+
assert abs(result - expected) < 1e-3
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex1 : Le chiffrement de César
|
|
3
|
+
=============================
|
|
4
|
+
|
|
5
|
+
Avant de parler de chiffrement homomorphe, il faut être à l'aise avec
|
|
6
|
+
l'arithmétique modulaire : c'est elle qui est derrière absolument tous les
|
|
7
|
+
schémas de chiffrement que vous verrez dans ce projet (Paillier, CKKS...).
|
|
8
|
+
|
|
9
|
+
Le chiffrement de César décale chaque lettre de `shift` positions dans
|
|
10
|
+
l'alphabet, en bouclant à la fin (Z + 1 = A). C'est exactement une addition
|
|
11
|
+
modulo 26.
|
|
12
|
+
|
|
13
|
+
À implémenter :
|
|
14
|
+
- caesar_encrypt(text: str, shift: int) -> str
|
|
15
|
+
- caesar_decrypt(text: str, shift: int) -> str
|
|
16
|
+
|
|
17
|
+
Règles :
|
|
18
|
+
- Les lettres minuscules et majuscules sont décalées, chacune restant
|
|
19
|
+
dans sa casse ('a'..'z' / 'A'..'Z').
|
|
20
|
+
- Tout ce qui n'est pas une lettre (espaces, ponctuation, chiffres,
|
|
21
|
+
lettres accentuées) est recopié SANS modification.
|
|
22
|
+
- `shift` peut être négatif ou plus grand que 26 : ça doit fonctionner
|
|
23
|
+
quand même (pensez à l'opérateur %).
|
|
24
|
+
|
|
25
|
+
Exemple :
|
|
26
|
+
caesar_encrypt("Hello, World!", 3) == "Khoor, Zruog!"
|
|
27
|
+
caesar_decrypt("Khoor, Zruog!", 3) == "Hello, World!"
|
|
28
|
+
|
|
29
|
+
Indice : pour une lettre `c`, sa position dans l'alphabet est
|
|
30
|
+
`ord(c) - ord('a')` (ou ord('A') pour les majuscules), et la nouvelle
|
|
31
|
+
position est `(position + shift) % 26`.
|
|
32
|
+
|
|
33
|
+
ATTENTION : pour détecter si `c` est une lettre, préférez une comparaison
|
|
34
|
+
explicite ('a' <= c <= 'z') plutôt que c.islower() / c.isupper() : ces
|
|
35
|
+
méthodes renvoient aussi True pour des lettres accentuées comme « é » ou
|
|
36
|
+
« à », qui doivent pourtant rester inchangées.
|
|
37
|
+
|
|
38
|
+
Testez avec : fhecli run ex1_cesar
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# I AM NOT DONE
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def caesar_encrypt(text: str, shift: int) -> str:
|
|
45
|
+
"""Décale chaque lettre de `text` de `shift` positions (modulo 26)."""
|
|
46
|
+
# TODO: implémentez cette fonction
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def caesar_decrypt(text: str, shift: int) -> str:
|
|
51
|
+
"""Opération inverse de caesar_encrypt."""
|
|
52
|
+
# TODO: implémentez cette fonction
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_known_vector():
|
|
62
|
+
assert caesar_encrypt("Hello, World!", 3) == "Khoor, Zruog!"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_decrypt_is_inverse_of_encrypt():
|
|
66
|
+
for shift in [1, 3, 13, 25]:
|
|
67
|
+
for text in ["Bonjour", "PYTHON is FUN", "a b c Z"]:
|
|
68
|
+
enc = caesar_encrypt(text, shift)
|
|
69
|
+
assert caesar_decrypt(enc, shift) == text
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_non_letters_untouched():
|
|
73
|
+
assert caesar_encrypt("123 !? éà", 5) == "123 !? éà"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_wraps_around_alphabet():
|
|
77
|
+
assert caesar_encrypt("xyz", 3) == "abc"
|
|
78
|
+
assert caesar_encrypt("XYZ", 3) == "ABC"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_negative_and_large_shift():
|
|
82
|
+
assert caesar_encrypt("abc", -1) == caesar_encrypt("abc", 25)
|
|
83
|
+
assert caesar_encrypt("abc", 29) == caesar_encrypt("abc", 3)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_case_is_preserved():
|
|
87
|
+
result = caesar_encrypt("AbC", 1)
|
|
88
|
+
assert result[0].isupper() and result[1].islower() and result[2].isupper()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_accented_letters_are_not_shifted():
|
|
92
|
+
assert caesar_encrypt("café", 1) == "dbgé"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex2 : XOR et le danger de la malléabilité
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
Le XOR (^) a une propriété étonnante : si `k` est une clé et `m` un message
|
|
6
|
+
(en octets), alors :
|
|
7
|
+
|
|
8
|
+
chiffrer(m, k) = m XOR k
|
|
9
|
+
déchiffrer(c, k) = c XOR k (car (m XOR k) XOR k == m)
|
|
10
|
+
|
|
11
|
+
C'est un chiffrement « homomorphe » jouet... mais c'est justement ce qui le
|
|
12
|
+
rend dangereux dès qu'on réutilise la même clé deux fois : c'est l'attaque
|
|
13
|
+
classique du « two-time pad ».
|
|
14
|
+
|
|
15
|
+
À implémenter :
|
|
16
|
+
- xor_bytes(a: bytes, b: bytes) -> bytes
|
|
17
|
+
XOR octet par octet de deux suites de même longueur.
|
|
18
|
+
- xor_encrypt(message: bytes, key: bytes) -> bytes
|
|
19
|
+
XOR du message avec la clé répétée autant de fois que nécessaire pour
|
|
20
|
+
couvrir sa longueur. (xor_decrypt est déjà fourni : le XOR est son
|
|
21
|
+
propre inverse.)
|
|
22
|
+
- reveal_xor_of_plaintexts(c1: bytes, c2: bytes) -> bytes
|
|
23
|
+
Si c1 et c2 sont deux messages chiffrés AVEC LA MÊME clé, cette
|
|
24
|
+
fonction doit renvoyer le XOR des deux messages en clair, SANS jamais
|
|
25
|
+
connaître la clé. (Indice : une seule ligne, qui réutilise xor_bytes.)
|
|
26
|
+
|
|
27
|
+
Pourquoi c'est important : ceci illustre une idée centrale du projet. Dans
|
|
28
|
+
un chiffrement classique, toute malléabilité est une faille de sécurité.
|
|
29
|
+
Ici, réutiliser la clé permet de retrouver de l'information sur les
|
|
30
|
+
messages en clair sans jamais les déchiffrer. Le FHE renverse ce principe :
|
|
31
|
+
il rend le chiffré délibérément et strictement malléable, mais uniquement
|
|
32
|
+
pour les opérations mathématiques choisies par le concepteur du schéma
|
|
33
|
+
(addition, multiplication...), avec des garanties de sécurité prouvées.
|
|
34
|
+
C'est toute la différence entre une malléabilité subie (dangereuse) et une
|
|
35
|
+
malléabilité conçue (le FHE).
|
|
36
|
+
|
|
37
|
+
Testez avec : fhecli run ex2_xor_malleability
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# I AM NOT DONE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def xor_bytes(a: bytes, b: bytes) -> bytes:
|
|
44
|
+
"""XOR octet par octet. `a` et `b` ont la même longueur."""
|
|
45
|
+
# TODO: implémentez cette fonction
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def xor_encrypt(message: bytes, key: bytes) -> bytes:
|
|
50
|
+
"""Chiffre `message` en le XORant avec `key` répétée."""
|
|
51
|
+
# TODO: implémentez cette fonction (réutilisez xor_bytes)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def xor_decrypt(ciphertext: bytes, key: bytes) -> bytes:
|
|
56
|
+
"""Le XOR est son propre inverse : déchiffrer = chiffrer à nouveau."""
|
|
57
|
+
return xor_encrypt(ciphertext, key)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def reveal_xor_of_plaintexts(c1: bytes, c2: bytes) -> bytes:
|
|
61
|
+
"""
|
|
62
|
+
Si c1 et c2 ont été chiffrés avec LA MÊME clé (two-time pad), renvoie
|
|
63
|
+
le XOR des deux messages en clair, sans jamais connaître la clé.
|
|
64
|
+
"""
|
|
65
|
+
# TODO: implémentez cette fonction
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_xor_bytes_basic():
|
|
75
|
+
assert xor_bytes(b"\x00\xff", b"\xff\x00") == b"\xff\xff"
|
|
76
|
+
assert xor_bytes(b"AB", b"AB") == b"\x00\x00"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_encrypt_decrypt_roundtrip():
|
|
80
|
+
key = b"secret"
|
|
81
|
+
for message in [b"hello world", b"a", b"", b"x" * 50]:
|
|
82
|
+
enc = xor_encrypt(message, key)
|
|
83
|
+
assert xor_decrypt(enc, key) == message
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_key_is_repeated():
|
|
87
|
+
key = b"ab"
|
|
88
|
+
message = b"1234"
|
|
89
|
+
enc = xor_encrypt(message, key)
|
|
90
|
+
assert enc == xor_bytes(message, b"abab")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_two_time_pad_attack():
|
|
94
|
+
key = b"supersecretkey!!"
|
|
95
|
+
p1 = b"TRANSFER 100 EUR"
|
|
96
|
+
p2 = b"TRANSFER 999 EUR"
|
|
97
|
+
c1 = xor_encrypt(p1, key)
|
|
98
|
+
c2 = xor_encrypt(p2, key)
|
|
99
|
+
|
|
100
|
+
# sans connaître `key`, on doit retrouver p1 XOR p2
|
|
101
|
+
revealed = reveal_xor_of_plaintexts(c1, c2)
|
|
102
|
+
assert revealed == xor_bytes(p1, p2)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex3 : Chiffrement authentifié avec Fernet
|
|
3
|
+
===========================================
|
|
4
|
+
|
|
5
|
+
Dans la démo web du projet Scoring FHE (04_demo/backend/app/crypto/classic_aes.py),
|
|
6
|
+
on utilise Fernet (AES-128-CBC + HMAC) pour montrer qu'un chiffrement
|
|
7
|
+
classique détecte et rejette toute donnée falsifiée. Ici, vous allez
|
|
8
|
+
reproduire ce comportement vous-même, avec la vraie bibliothèque
|
|
9
|
+
`cryptography`.
|
|
10
|
+
|
|
11
|
+
À implémenter :
|
|
12
|
+
- encrypt_value(value: str, key: bytes) -> bytes
|
|
13
|
+
Chiffre une chaîne avec Fernet(key). `value` doit être encodée en
|
|
14
|
+
bytes avant chiffrement (value.encode()).
|
|
15
|
+
- decrypt_value(token: bytes, key: bytes) -> str
|
|
16
|
+
Opération inverse, renvoie une chaîne (.decode()).
|
|
17
|
+
- tamper_and_check(token: bytes, key: bytes) -> bool
|
|
18
|
+
Modifie UN SEUL octet du token (par exemple en inversant ses bits
|
|
19
|
+
avec XOR 0xFF), puis tente de le déchiffrer. Renvoie True si la
|
|
20
|
+
falsification est détectée (exception InvalidToken levée), False
|
|
21
|
+
sinon.
|
|
22
|
+
|
|
23
|
+
Indice :
|
|
24
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
25
|
+
key = Fernet.generate_key() # à utiliser dans vos propres tests
|
|
26
|
+
|
|
27
|
+
Pour modifier un octet d'un objet bytes (immuable), passez par un bytearray :
|
|
28
|
+
data = bytearray(token)
|
|
29
|
+
data[0] ^= 0xFF
|
|
30
|
+
tampered = bytes(data)
|
|
31
|
+
|
|
32
|
+
Ce que ça démontre : un chiffrement authentifié ne laisse aucune marge de
|
|
33
|
+
manœuvre. La moindre modification du chiffré, même d'un seul bit, est
|
|
34
|
+
détectée. C'est une excellente propriété de sécurité... mais c'est aussi
|
|
35
|
+
précisément ce qui rend tout calcul sur le chiffré impossible avec ce type
|
|
36
|
+
de schéma.
|
|
37
|
+
|
|
38
|
+
Testez avec : fhecli run ex3_fernet_tamper
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
42
|
+
|
|
43
|
+
# I AM NOT DONE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def encrypt_value(value: str, key: bytes) -> bytes:
|
|
47
|
+
"""Chiffre `value` avec Fernet(key)."""
|
|
48
|
+
# TODO: implémentez cette fonction
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decrypt_value(token: bytes, key: bytes) -> str:
|
|
53
|
+
"""Déchiffre `token` avec Fernet(key)."""
|
|
54
|
+
# TODO: implémentez cette fonction
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def tamper_and_check(token: bytes, key: bytes) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Modifie un octet de `token`, tente de le déchiffrer, et renvoie True
|
|
61
|
+
si InvalidToken a bien été levée (falsification détectée).
|
|
62
|
+
"""
|
|
63
|
+
# TODO: implémentez cette fonction
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
import pytest
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_roundtrip():
|
|
75
|
+
key = Fernet.generate_key()
|
|
76
|
+
token = encrypt_value("hello bank", key)
|
|
77
|
+
assert isinstance(token, bytes)
|
|
78
|
+
assert decrypt_value(token, key) == "hello bank"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_different_keys_cannot_decrypt():
|
|
82
|
+
key1 = Fernet.generate_key()
|
|
83
|
+
key2 = Fernet.generate_key()
|
|
84
|
+
token = encrypt_value("secret data", key1)
|
|
85
|
+
with pytest.raises(Exception):
|
|
86
|
+
decrypt_value(token, key2)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_tampering_is_detected():
|
|
90
|
+
key = Fernet.generate_key()
|
|
91
|
+
token = encrypt_value("revenu=4000", key)
|
|
92
|
+
assert tamper_and_check(token, key) is True
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex4 : Exponentiation modulaire rapide (square-and-multiply)
|
|
3
|
+
============================================================
|
|
4
|
+
|
|
5
|
+
On quitte le chiffrement classique pour poser les briques mathématiques du
|
|
6
|
+
chiffrement homomorphe. La toute première est l'exponentiation modulaire :
|
|
7
|
+
calculer (base ** exp) % mod. Paillier, RSA et beaucoup d'autres schémas
|
|
8
|
+
reposent dessus, avec des exposants de plusieurs centaines de chiffres.
|
|
9
|
+
|
|
10
|
+
Calculer base ** exp puis prendre le modulo est impossible en pratique : le
|
|
11
|
+
nombre intermédiaire aurait des millions de chiffres. L'algorithme
|
|
12
|
+
« square-and-multiply » garde au contraire des nombres petits (toujours
|
|
13
|
+
réduits modulo `mod`) en parcourant l'exposant bit par bit.
|
|
14
|
+
|
|
15
|
+
À implémenter :
|
|
16
|
+
- mod_pow(base, exp, mod) -> int
|
|
17
|
+
Calcule (base ** exp) % mod SANS passer par base ** exp.
|
|
18
|
+
|
|
19
|
+
Algorithme :
|
|
20
|
+
result = 1
|
|
21
|
+
base = base % mod
|
|
22
|
+
tant que exp > 0 :
|
|
23
|
+
si le bit de poids faible de exp vaut 1 (exp & 1) :
|
|
24
|
+
result = (result * base) % mod
|
|
25
|
+
base = (base * base) % mod # on élève base au carré
|
|
26
|
+
exp = exp // 2 # on passe au bit suivant
|
|
27
|
+
|
|
28
|
+
Contrainte : n'utilisez pas le pow(base, exp, mod) intégré à trois arguments
|
|
29
|
+
(c'est justement ce que vous reconstruisez ici). Vous pourrez l'utiliser dans
|
|
30
|
+
les exercices suivants.
|
|
31
|
+
|
|
32
|
+
Testez avec : fhecli run ex4_modpow
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# I AM NOT DONE
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def mod_pow(base: int, exp: int, mod: int) -> int:
|
|
39
|
+
"""Calcule (base ** exp) % mod par exponentiation rapide."""
|
|
40
|
+
# TODO: implémentez l'algorithme square-and-multiply décrit ci-dessus
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
import random
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_small_values():
|
|
52
|
+
assert mod_pow(4, 13, 497) == pow(4, 13, 497)
|
|
53
|
+
assert mod_pow(2, 10, 1000) == pow(2, 10, 1000)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_exponent_zero():
|
|
57
|
+
assert mod_pow(123, 0, 97) == 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_matches_builtin_pow_on_random_values():
|
|
61
|
+
random.seed(42)
|
|
62
|
+
for _ in range(30):
|
|
63
|
+
base = random.randint(2, 10_000)
|
|
64
|
+
exp = random.randint(0, 1000)
|
|
65
|
+
mod = random.randint(2, 10_000)
|
|
66
|
+
assert mod_pow(base, exp, mod) == pow(base, exp, mod)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_large_numbers_like_real_crypto():
|
|
70
|
+
base = 123456789012345678901234567890
|
|
71
|
+
exp = 987654321
|
|
72
|
+
mod = 100000000000000000000000000000000000039
|
|
73
|
+
assert mod_pow(base, exp, mod) == pow(base, exp, mod)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex5 : PGCD étendu et inverse modulaire
|
|
3
|
+
=======================================
|
|
4
|
+
|
|
5
|
+
Deuxième brique mathématique. Dans Paillier, le déchiffrement a besoin de
|
|
6
|
+
« diviser » modulo n, c'est-à-dire de multiplier par un inverse modulaire :
|
|
7
|
+
l'entier x tel que (a * x) % mod == 1. Cet inverse n'existe que si a et mod
|
|
8
|
+
sont premiers entre eux (pgcd == 1).
|
|
9
|
+
|
|
10
|
+
On l'obtient avec l'algorithme d'Euclide étendu, qui calcule en même temps le
|
|
11
|
+
pgcd de a et b et les coefficients de Bézout x, y tels que a*x + b*y == pgcd.
|
|
12
|
+
|
|
13
|
+
À implémenter :
|
|
14
|
+
- extended_gcd(a, b) -> (g, x, y)
|
|
15
|
+
avec a*x + b*y == g == pgcd(a, b). Version récursive :
|
|
16
|
+
si b == 0 : renvoyer (a, 1, 0)
|
|
17
|
+
sinon :
|
|
18
|
+
g, x1, y1 = extended_gcd(b, a % b)
|
|
19
|
+
renvoyer (g, y1, x1 - (a // b) * y1)
|
|
20
|
+
- mod_inverse(a, mod) -> int
|
|
21
|
+
Réutilise extended_gcd. Si g != 1, l'inverse n'existe pas : levez une
|
|
22
|
+
ValueError. Sinon, renvoyez x % mod (le % ramène l'inverse dans
|
|
23
|
+
[0, mod) même si x est négatif).
|
|
24
|
+
|
|
25
|
+
Testez avec : fhecli run ex5_modinv
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# I AM NOT DONE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extended_gcd(a: int, b: int) -> tuple[int, int, int]:
|
|
32
|
+
"""Renvoie (g, x, y) tel que a*x + b*y == g == pgcd(a, b)."""
|
|
33
|
+
# TODO: implémentez la récursion décrite ci-dessus
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mod_inverse(a: int, mod: int) -> int:
|
|
38
|
+
"""Renvoie x tel que (a * x) % mod == 1. Lève ValueError si impossible."""
|
|
39
|
+
# TODO: réutilisez extended_gcd pour calculer l'inverse
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
import math
|
|
48
|
+
import random
|
|
49
|
+
|
|
50
|
+
import pytest
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_extended_gcd_bezout_identity():
|
|
54
|
+
for a, b in [(35, 15), (240, 46), (17, 5), (1000000007, 998244353)]:
|
|
55
|
+
g, x, y = extended_gcd(a, b)
|
|
56
|
+
assert a * x + b * y == g
|
|
57
|
+
assert g == math.gcd(a, b)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_mod_inverse_matches_builtin():
|
|
61
|
+
random.seed(7)
|
|
62
|
+
mod = 1_000_000_007 # nombre premier
|
|
63
|
+
for _ in range(20):
|
|
64
|
+
a = random.randint(1, mod - 1)
|
|
65
|
+
assert mod_inverse(a, mod) % mod == pow(a, -1, mod)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_mod_inverse_paillier_like_modulus():
|
|
69
|
+
n = 1009 * 1013
|
|
70
|
+
for a in [5, 123, 99999]:
|
|
71
|
+
inv = mod_inverse(a, n)
|
|
72
|
+
assert (a * inv) % n == 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_mod_inverse_raises_when_not_coprime():
|
|
76
|
+
with pytest.raises(ValueError):
|
|
77
|
+
mod_inverse(6, 9) # pgcd(6, 9) == 3, pas d'inverse
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ex6 : Générer une paire de clés Paillier
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
On attaque le chiffrement homomorphe partiel de Paillier (1999). Sa propriété
|
|
6
|
+
remarquable : on peut ADDITIONNER deux messages chiffrés sans les déchiffrer.
|
|
7
|
+
C'est exactement ce qu'il faut pour un score = somme pondérée de features.
|
|
8
|
+
|
|
9
|
+
Tout commence par les clés. On part de deux nombres premiers p et q (petits
|
|
10
|
+
ici, pour que les tests soient rapides ; énormes en vrai).
|
|
11
|
+
|
|
12
|
+
Les deux fonctions utilitaires _extended_gcd et _mod_inverse sont déjà
|
|
13
|
+
fournies (c'est votre travail de l'ex5). Vous n'implémentez que la génération
|
|
14
|
+
de clés.
|
|
15
|
+
|
|
16
|
+
À implémenter :
|
|
17
|
+
- generate_keypair(p, q) -> (public_key, private_key)
|
|
18
|
+
avec la variante classique g = n + 1 :
|
|
19
|
+
n = p * q
|
|
20
|
+
g = n + 1
|
|
21
|
+
lam = (p - 1) * (q - 1) # lambda = indicatrice d'Euler
|
|
22
|
+
mu = inverse modulaire de lam modulo n
|
|
23
|
+
puis :
|
|
24
|
+
public_key = {"n": n, "g": g, "n2": n * n}
|
|
25
|
+
private_key = {"lam": lam, "mu": mu}
|
|
26
|
+
|
|
27
|
+
Testez avec : fhecli run ex6_paillier_keygen
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# I AM NOT DONE
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extended_gcd(a: int, b: int) -> tuple[int, int, int]:
|
|
34
|
+
"""Fourni (ex5) : (g, x, y) tel que a*x + b*y == g == pgcd(a, b)."""
|
|
35
|
+
if b == 0:
|
|
36
|
+
return a, 1, 0
|
|
37
|
+
g, x1, y1 = _extended_gcd(b, a % b)
|
|
38
|
+
return g, y1, x1 - (a // b) * y1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mod_inverse(a: int, mod: int) -> int:
|
|
42
|
+
"""Fourni (ex5) : inverse modulaire de a modulo mod."""
|
|
43
|
+
g, x, _ = _extended_gcd(a, mod)
|
|
44
|
+
if g != 1:
|
|
45
|
+
raise ValueError(f"{a} n'a pas d'inverse modulo {mod}")
|
|
46
|
+
return x % mod
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def generate_keypair(p: int, q: int) -> tuple[dict, dict]:
|
|
50
|
+
"""Construit une paire de clés Paillier simplifiée (g = n + 1)."""
|
|
51
|
+
# TODO: calculez n, g, lam, mu et renvoyez (public_key, private_key)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Tests : ne modifiez pas cette section, elle valide votre implémentation.
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
P, Q = 1009, 1013
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_public_key_values():
|
|
63
|
+
pub, _ = generate_keypair(P, Q)
|
|
64
|
+
n = P * Q
|
|
65
|
+
assert pub["n"] == n
|
|
66
|
+
assert pub["g"] == n + 1
|
|
67
|
+
assert pub["n2"] == n * n
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_private_key_values():
|
|
71
|
+
_, priv = generate_keypair(P, Q)
|
|
72
|
+
lam = (P - 1) * (Q - 1)
|
|
73
|
+
n = P * Q
|
|
74
|
+
assert priv["lam"] == lam
|
|
75
|
+
# mu doit être l'inverse modulaire de lam mod n (vérifié indépendamment)
|
|
76
|
+
assert (priv["mu"] * lam) % n == 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_keys_are_consistent_for_other_primes():
|
|
80
|
+
p, q = 61, 53
|
|
81
|
+
pub, priv = generate_keypair(p, q)
|
|
82
|
+
n = p * q
|
|
83
|
+
lam = (p - 1) * (q - 1)
|
|
84
|
+
assert pub["n"] == n
|
|
85
|
+
assert (priv["mu"] * lam) % n == 1
|