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.

Files changed (37) hide show
  1. fhe_cli-1.3.1.dist-info/METADATA +280 -0
  2. fhe_cli-1.3.1.dist-info/RECORD +37 -0
  3. fhe_cli-1.3.1.dist-info/WHEEL +5 -0
  4. fhe_cli-1.3.1.dist-info/entry_points.txt +2 -0
  5. fhe_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
  6. fhe_cli-1.3.1.dist-info/top_level.txt +1 -0
  7. fhecli/__init__.py +5 -0
  8. fhecli/__main__.py +6 -0
  9. fhecli/cli.py +220 -0
  10. fhecli/data/info.toml +77 -0
  11. fhecli/data/originals/ex10_ckks_bonus.py +84 -0
  12. fhecli/data/originals/ex1_cesar.py +92 -0
  13. fhecli/data/originals/ex2_xor_malleability.py +102 -0
  14. fhecli/data/originals/ex3_fernet_tamper.py +92 -0
  15. fhecli/data/originals/ex4_modpow.py +73 -0
  16. fhecli/data/originals/ex5_modinv.py +77 -0
  17. fhecli/data/originals/ex6_paillier_keygen.py +85 -0
  18. fhecli/data/originals/ex7_paillier_enc_dec.py +118 -0
  19. fhecli/data/originals/ex8_paillier_homomorphic.py +144 -0
  20. fhecli/data/originals/ex9_scoring_paillier.py +143 -0
  21. fhecli/data/solutions/ex10_ckks_bonus.sol +8 -0
  22. fhecli/data/solutions/ex1_cesar.sol +6 -0
  23. fhecli/data/solutions/ex2_xor_malleability.sol +7 -0
  24. fhecli/data/solutions/ex3_fernet_tamper.sol +7 -0
  25. fhecli/data/solutions/ex4_modpow.sol +5 -0
  26. fhecli/data/solutions/ex5_modinv.sol +6 -0
  27. fhecli/data/solutions/ex6_paillier_keygen.sol +8 -0
  28. fhecli/data/solutions/ex7_paillier_enc_dec.sol +11 -0
  29. fhecli/data/solutions/ex8_paillier_homomorphic.sol +12 -0
  30. fhecli/data/solutions/ex9_scoring_paillier.sol +15 -0
  31. fhecli/info.py +46 -0
  32. fhecli/resources.py +28 -0
  33. fhecli/runner.py +72 -0
  34. fhecli/solutions.py +30 -0
  35. fhecli/ui.py +70 -0
  36. fhecli/watch.py +116 -0
  37. 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