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,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: fhe-cli
3
+ Version: 1.3.1
4
+ Summary: Atelier d'exercices en ligne de commande, à la rustlings : chiffrement classique et chiffrement homomorphe (FHE)
5
+ Author: Ravin THILAGARASA, Thomas GRUNDSZTEJN
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Defl3ct-941/Scoring_FHE
8
+ Project-URL: Source, https://github.com/Defl3ct-941/Scoring_FHE/tree/main/03_cas_pratiques/fhe-exercices-cli
9
+ Project-URL: Issues, https://github.com/Defl3ct-941/Scoring_FHE/issues
10
+ Project-URL: GitLab, https://gitlab.securechat.fr/esgi/5a/Scoring_FHE
11
+ Project-URL: Changelog, https://github.com/Defl3ct-941/Scoring_FHE/blob/main/CHANGELOG.md
12
+ Keywords: fhe,chiffrement homomorphe,homomorphic encryption,cryptographie,cryptography,exercices,rustlings,enseignement,scoring
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Natural Language :: French
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
25
+ Classifier: Topic :: Education
26
+ Classifier: Topic :: Security :: Cryptography
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: pytest<10,>=9.2
31
+ Requires-Dist: cryptography<51,>=50.0
32
+ Provides-Extra: ckks
33
+ Requires-Dist: tenseal<0.4,>=0.3.16; extra == "ckks"
34
+ Dynamic: license-file
35
+
36
+ # fhecli : atelier CLI de chiffrement classique et FHE
37
+
38
+ Un atelier d'exercices en ligne de commande, inspiré de
39
+ [rustlings](https://github.com/rust-lang/rustlings) : vous éditez les fichiers
40
+ d'exercice dans votre propre éditeur, avec votre propre environnement Python,
41
+ et un petit outil (`fhecli`) surveille vos fichiers et relance les tests à
42
+ chaque sauvegarde.
43
+
44
+ Il accompagne le cours « Scoring financier sur données chiffrées via FHE »
45
+ (FYC, ESGI) : c'est la même progression pédagogique que l'atelier web du
46
+ projet, adaptée pour tourner directement dans votre terminal.
47
+
48
+ > **Version actuelle : 3 exercices.** Seul le module A (chiffrement
49
+ > classique) est publié pour le moment. Les modules suivants (arithmétique
50
+ > modulaire, Paillier from scratch, scoring chiffré, bonus CKKS) arriveront
51
+ > dans une prochaine mise à jour.
52
+
53
+ ## Installation
54
+
55
+ Python 3.11 ou plus récent est requis.
56
+
57
+ ```bash
58
+ python -m venv .venv
59
+ # Windows
60
+ .venv\Scripts\activate
61
+ # macOS / Linux
62
+ source .venv/bin/activate
63
+
64
+ pip install fhe-cli
65
+ fhecli init # crée le dossier ./fhe-exercices avec les exercices
66
+ cd fhe-exercices
67
+ fhecli watch
68
+ ```
69
+
70
+ Le paquet s'installe sous le nom **`fhe-cli`** ; la commande et le module
71
+ Python restent `fhecli`.
72
+
73
+ `fhecli init [dossier]` crée un **espace de travail** : le dossier
74
+ `exercises/` (vos fichiers), un `pytest.ini` et un fichier marqueur
75
+ `fhecli.toml`. Les commandes de `fhecli` retrouvent cet espace depuis n'importe
76
+ quel sous-dossier. `fhecli init` refuse d'écraser un espace existant.
77
+
78
+ ### Sans Python : exécutable autonome
79
+
80
+ Pour chaque OS, un exécutable unique embarque tout (Python, `fhecli`,
81
+ `pytest`, `cryptography` et les exercices) : **aucune installation de Python
82
+ n'est nécessaire**. Téléchargez celui de votre système
83
+ (`fhecli-<version>-windows-x86_64.exe`, `-linux-x86_64`, `-macos-arm64` ou
84
+ `-macos-x86_64`), puis :
85
+
86
+ ```bash
87
+ # Windows (PowerShell)
88
+ . hecli-1.2.0-windows-x86_64.exe init
89
+ # macOS / Linux
90
+ chmod +x fhecli-1.2.0-linux-x86_64
91
+ ./fhecli-1.2.0-linux-x86_64 init
92
+ ```
93
+
94
+ Renommez-le en `fhecli` (ou `fhecli.exe`) et placez-le dans un dossier du
95
+ `PATH` pour l'utiliser comme dans les exemples ci-dessous. Le code et les
96
+ solutions y sont embarqués compressés, jamais en clair. Sous macOS, au premier
97
+ lancement, autorisez le binaire dans Réglages système > Confidentialité et
98
+ sécurité (il n'est pas signé).
99
+
100
+ ## Utilisation
101
+
102
+ ### Mode guidé (recommandé pour commencer)
103
+
104
+ ```bash
105
+ fhecli watch
106
+ ```
107
+
108
+ Affiche l'exercice courant, relance ses tests à chaque fois que vous
109
+ sauvegardez le fichier, et passe automatiquement au suivant une fois qu'il est
110
+ résolu (en proposant alors d'afficher la solution de référence pour comparer).
111
+ Ctrl+C pour quitter à tout moment : rien n'est perdu, vos fichiers restent sur
112
+ le disque.
113
+
114
+ ### Autres commandes
115
+
116
+ ```bash
117
+ fhecli list # vue d'ensemble des exercices et de leur statut
118
+ fhecli run ex1_cesar # lance les tests d'un seul exercice
119
+ fhecli hint ex1_cesar # affiche un indice ciblé
120
+ fhecli solution ex1_cesar # affiche la solution (une fois l'exercice résolu)
121
+ fhecli reset ex1_cesar # remet CET exercice à son énoncé de départ (avec confirmation)
122
+ fhecli reset --all # remet TOUS les exercices à leur énoncé de départ (avec confirmation)
123
+ fhecli reset --all -y # idem, sans confirmation
124
+ fhecli --version
125
+ ```
126
+
127
+ Codes de sortie de `fhecli run` : `0` exercice résolu, `1` tests en échec
128
+ (ou erreur), `2` tests OK mais ligne `# I AM NOT DONE` encore présente.
129
+
130
+ Vous pouvez aussi lancer les tests vous-même avec `python -m pytest` depuis
131
+ l'espace de travail : le `pytest.ini` fourni collecte bien les exercices.
132
+
133
+ ## Comment marche un exercice
134
+
135
+ Chaque exercice est un unique fichier `.py` dans `exercises/`, qui contient
136
+ tout :
137
+
138
+ 1. Un docstring en haut du fichier avec l'énoncé complet (contexte,
139
+ formules, ce qu'il faut implémenter, indices).
140
+ 2. Une ligne `# I AM NOT DONE` juste après : tant qu'elle est présente,
141
+ l'exercice est considéré comme « pas terminé », même si les tests passent
142
+ déjà. **Retirez-la vous-même** une fois satisfait de votre solution :
143
+ c'est un geste délibérément manuel, pour vous forcer à vraiment valider
144
+ votre travail.
145
+ 3. La ou les fonctions à compléter (marquées `raise NotImplementedError`).
146
+ 4. Une section de tests en bas du fichier (à ne pas modifier), exécutée par
147
+ `pytest` à chaque `fhecli run` / `fhecli watch`.
148
+
149
+ ## Solutions
150
+
151
+ La solution de référence d'un exercice est **verrouillée** tant que
152
+ l'exercice n'est pas résolu : `fhecli solution` ne l'affiche qu'une fois que
153
+ les tests passent **et** que la ligne `# I AM NOT DONE` a été retirée.
154
+
155
+ Dans le paquet, les solutions ne sont pas stockées en clair : elles sont
156
+ compressées et encodées (zlib + base64). C'est une **dissuasion**, pour
157
+ éviter de tomber sur la correction par hasard en fouillant le dossier du
158
+ paquet, **pas une protection** : rien n'empêche quelqu'un de déterminé de les
159
+ décoder. Le vrai garde-fou reste pédagogique.
160
+
161
+ ## Progression (dans l'ordre suivi par `fhecli watch`)
162
+
163
+ | # | Exercice | Catégorie |
164
+ |---|----------|-----------|
165
+ | 1 | ex1_cesar | A. Chiffrement classique |
166
+ | 2 | ex2_xor_malleability | A. Chiffrement classique |
167
+ | 3 | ex3_fernet_tamper | A. Chiffrement classique |
168
+ | 4 | ex4_modpow | B. Briques mathématiques |
169
+ | 5 | ex5_modinv | B. Briques mathématiques |
170
+ | 6 | ex6_paillier_keygen | C. Paillier, pas à pas |
171
+ | 7 | ex7_paillier_enc_dec | C. Paillier, pas à pas |
172
+ | 8 | ex8_paillier_homomorphic | C. Paillier, pas à pas |
173
+ | 9 | ex9_scoring_paillier | D. Scoring chiffré |
174
+ | 10 | ex10_ckks_bonus | E. Bonus : FHE réel avec CKKS |
175
+
176
+ Le parcours va du chiffrement classique (où tout calcul sur le chiffré est
177
+ impossible) à Paillier reconstruit à la main, puis à un score bancaire chiffré
178
+ de bout en bout, et enfin à un vrai calcul FHE avec CKKS.
179
+
180
+ Le bonus **ex10 (CKKS)** s'appuie sur [TenSEAL](https://github.com/OpenMined/TenSEAL),
181
+ une dépendance native qui n'est pas installée par défaut (et qui n'est pas
182
+ embarquée dans les exécutables autonomes). Pour le faire :
183
+
184
+ ```bash
185
+ pip install "fhe-cli[ckks]"
186
+ ```
187
+
188
+ Sans TenSEAL, les tests de l'ex10 sont simplement ignorés (skipped).
189
+
190
+ ## Développement (depuis le dépôt Scoring FHE)
191
+
192
+ Le dossier `03_cas_pratiques/fhe-exercices-cli` du dépôt est lui-même un
193
+ espace de travail (il contient `fhecli.toml`, `pytest.ini` et `exercises/`) :
194
+
195
+ ```bash
196
+ cd 03_cas_pratiques/fhe-exercices-cli
197
+ python -m venv .venv
198
+ .venv\Scripts\activate # macOS / Linux : source .venv/bin/activate
199
+ pip install -e .
200
+ fhecli watch
201
+ python -m pytest tests -q # tests de l'outil lui-même
202
+ ```
203
+
204
+ Avant de commiter : `fhecli reset --all -y`, pour ne jamais versionner de
205
+ réponses dans `exercises/`.
206
+
207
+ ```
208
+ fhe-exercices-cli/
209
+ ├── pyproject.toml # empaquetage (PyPI), point d'entrée `fhecli`
210
+ ├── LICENSE, MANIFEST.in
211
+ ├── fhecli/ # le paquet
212
+ │ ├── cli.py # commandes : init, watch, list, run, hint, solution, reset
213
+ │ ├── workspace.py # création et recherche de l'espace de travail
214
+ │ ├── watch.py # boucle de surveillance des fichiers
215
+ │ ├── runner.py # exécution de pytest en sous-processus isolé
216
+ │ ├── info.py, resources.py # liste des exercices, accès aux données du paquet
217
+ │ ├── solutions.py # encodage / décodage des solutions
218
+ │ ├── ui.py # couleurs, UTF-8, confirmations
219
+ │ └── data/ # embarqué dans le paquet (lecture seule)
220
+ │ ├── info.toml # métadonnées des exercices (titre, catégorie, indice)
221
+ │ ├── originals/ # énoncés d'origine (fhecli init / reset)
222
+ │ └── solutions/ # solutions ENCODÉES (.sol)
223
+ ├── solutions_src/ # solutions en clair : dans le dépôt, JAMAIS dans la wheel ni le sdist
224
+ ├── scripts/ # encode_solutions.py, build_binary.py, check_binary.py
225
+ ├── packaging/ # spécification PyInstaller (exécutables autonomes)
226
+ ├── tests/ # tests automatisés de l'outil
227
+ ├── exercises/ # espace de travail de développement
228
+ ├── fhecli.toml, pytest.ini # marqueur et configuration de cet espace
229
+ └── requirements.txt
230
+ ```
231
+
232
+ Après toute modification d'une solution dans `solutions_src/` :
233
+
234
+ ```bash
235
+ python scripts/encode_solutions.py
236
+ ```
237
+
238
+ Construire le paquet (sans le publier) :
239
+
240
+ ```bash
241
+ python -m pip install build
242
+ python -m build # dist/fhe_cli-<version>-py3-none-any.whl et .tar.gz
243
+ ```
244
+
245
+ ## Exécutables, CI et publication
246
+
247
+ Construire et vérifier l'exécutable de l'OS courant (PyInstaller ne fait pas
248
+ de compilation croisée) :
249
+
250
+ ```bash
251
+ pip install -e . pyinstaller
252
+ python scripts/build_binary.py # dist/fhecli-<version>-<os>-<arch>[.exe]
253
+ ```
254
+
255
+ `scripts/build_binary.py` lance ensuite `scripts/check_binary.py`, qui
256
+ rejoue le parcours d'un étudiant avec le binaire seul (sans Python dans le
257
+ `PATH`) et vérifie qu'aucun source, aucune solution ni aucun énoncé n'apparaît
258
+ en clair dans le fichier.
259
+
260
+ La CI **GitHub Actions** (`.github/workflows/fhecli.yml`) fait tout cela
261
+ automatiquement : tests sur Linux, Windows et macOS (Python 3.11 et 3.15),
262
+ exécutables Windows, Linux, macOS arm64 et macOS x86_64, wheel et sdist.
263
+
264
+ Pour publier une version : mettre à jour `fhecli.__version__` et le
265
+ CHANGELOG, puis pousser un tag `fhecli-vX.Y.Z` (il doit correspondre à la
266
+ version). GitHub crée alors une release avec les exécutables et publie sur
267
+ PyPI. Une version publiée sur PyPI ne peut pas être republiée : en cas de
268
+ problème, publier la version suivante.
269
+
270
+ Configuration à faire une seule fois :
271
+
272
+ - PyPI, « trusted publishing » : sur pypi.org, ajouter un *pending publisher*
273
+ pour le projet `fhecli` (dépôt `Defl3ct-941/Scoring_FHE`, workflow
274
+ `fhecli.yml`, environnement `pypi`), et créer l'environnement `pypi` dans
275
+ les réglages du dépôt GitHub (limité aux tags `fhecli-v*`). Aucun jeton
276
+ API n'est nécessaire.
277
+
278
+ ## Licence
279
+
280
+ MIT, voir [LICENSE](LICENSE).
@@ -0,0 +1,37 @@
1
+ fhe_cli-1.3.1.dist-info/licenses/LICENSE,sha256=vWfv3xoZi_mWXZkX8miHXnN_V6U6cAi6gLb3YhgiZys,1094
2
+ fhecli/__init__.py,sha256=KsQ8hLzDhFfh7u8kltwTRXdOzHkbD_MTvZbjIJstyw8,177
3
+ fhecli/__main__.py,sha256=r6-2p5dAcP4a77CVKwjBx9EmW267PfbI9bTPBJQvC2o,119
4
+ fhecli/cli.py,sha256=U3dj7bWb5WQM26-UH31GCuilkb9iZKrV8ehYqviMyCo,8223
5
+ fhecli/info.py,sha256=3iVuPgPIzGl3KMODPOYAJjm2vzj2meEDsTI3Z-0U16o,1201
6
+ fhecli/resources.py,sha256=FcCEhubBAeEhL9aCCweJOz-H49zptA6o6usVmY-DNQw,842
7
+ fhecli/runner.py,sha256=m_jkNm1Pgb7KSm1890KieCVGCMxGZ2dZQ4LwcvaZyMA,2371
8
+ fhecli/solutions.py,sha256=1LyTfymz8AbZAWoL3v1elzBh2yxxQHjabbyDO36nTA4,1173
9
+ fhecli/ui.py,sha256=nZC5y4lOj40MXrwvIkoscnIIHQUlVRvB_8OQHYUgSjg,1760
10
+ fhecli/watch.py,sha256=6Dg37DiEBV2luN0mzHc_F3KUFlHQ4k0EOtYi_ecKJYk,4422
11
+ fhecli/workspace.py,sha256=dnaZRfQIo13JMv6u3Thf59ASOzqMTaDGNPP5mG2KSFU,2059
12
+ fhecli/data/info.toml,sha256=yk9RDSMCHvFrjG9pD2JJrzpPpW514iyPyzfLLtHO4QQ,4024
13
+ fhecli/data/originals/ex10_ckks_bonus.py,sha256=-54y7tm7WvFKrkVwMMo5262cmknfdd_09qxmIOIAhms,3181
14
+ fhecli/data/originals/ex1_cesar.py,sha256=8fAbsJHWLTe5venxOINIDB5UU6HY_SEi1rrLOSGIklc,3165
15
+ fhecli/data/originals/ex2_xor_malleability.py,sha256=mLUOa-1c9KiIwd8pm9t-rPrWWz4STE_MAgGzHYbsaH4,3767
16
+ fhecli/data/originals/ex3_fernet_tamper.py,sha256=dSfH_zeHrsFVzJih0kyQ25O-7VJ6yv565S4RcMmigTI,3180
17
+ fhecli/data/originals/ex4_modpow.py,sha256=7E0MDUDDQIuwy_UfWa78W3ThfJYws9qovpl51ab-FLA,2526
18
+ fhecli/data/originals/ex5_modinv.py,sha256=e-zUlyF-1WnuY69r-wx1uTQdo3x1U-7y77ScrzDHKFo,2535
19
+ fhecli/data/originals/ex6_paillier_keygen.py,sha256=Fyhcd-rpZDDbzvzGDRrgspXGrv6LgIbu_wBq2a6LL7o,2723
20
+ fhecli/data/originals/ex7_paillier_enc_dec.py,sha256=gxQ3ciXMR5hFjbed4_nBlFt3MwIU_hnxiYb2XvCBodk,3670
21
+ fhecli/data/originals/ex8_paillier_homomorphic.py,sha256=HkUJEO-l-_m6y0mBJv5UtdMdmb8TMiZ75uvN24Pq8d4,4494
22
+ fhecli/data/originals/ex9_scoring_paillier.py,sha256=sZesevdix9KJOdbJ3ul4fBCguKIGN3xoStaJrEdDMmE,4497
23
+ fhecli/data/solutions/ex10_ckks_bonus.sol,sha256=Qc_Wehf7gmy5gkgbzB7weHLeg8zvlk7PJ2q14wDWyKU,545
24
+ fhecli/data/solutions/ex1_cesar.sol,sha256=uKkiDnpdzTw344mxT57k9BqHcm_KfW0JaMeOLcSLD7U,363
25
+ fhecli/data/solutions/ex2_xor_malleability.sol,sha256=ftzmK_TBaHumWqA39BvcxrNO2EJkUosijrkKyKQDUhA,440
26
+ fhecli/data/solutions/ex3_fernet_tamper.sol,sha256=BTpUkFwci0CxEuoRiLg4pf36-k_7TjR6sS760qo4wzk,424
27
+ fhecli/data/solutions/ex4_modpow.sol,sha256=SYBr20o1WesQSotVBoary2ErOTmpFDESjEq2K_T95sw,286
28
+ fhecli/data/solutions/ex5_modinv.sol,sha256=eZQLwqfghF7w8XpEydcLkO_LE-Gl3ehKm4yF7d-YWys,391
29
+ fhecli/data/solutions/ex6_paillier_keygen.sol,sha256=rEPnicNhe3xaHqAJn447zdr02uT_G0wlvc38OSllpu8,549
30
+ fhecli/data/solutions/ex7_paillier_enc_dec.sol,sha256=gnaw9EHj9h4S0ZmI4Y7W1UYuKTSQe9VYhk2_1y0U6xo,716
31
+ fhecli/data/solutions/ex8_paillier_homomorphic.sol,sha256=D0zoixYhg8PlmKbxROoT0hPGUAlN3nPzI9iHapq3_w0,817
32
+ fhecli/data/solutions/ex9_scoring_paillier.sol,sha256=9tBin-kI7puipz0VJ66cR7aeswpf71cCr7CVHhKhbDU,1068
33
+ fhe_cli-1.3.1.dist-info/METADATA,sha256=atRRMKbxGoO_SG7ry9AazKK82DvmdIRnzwbIV9cKgLM,11897
34
+ fhe_cli-1.3.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
35
+ fhe_cli-1.3.1.dist-info/entry_points.txt,sha256=Rhwd2ixRkUJlh3COEK8uOd72ZBYVqqjSB0Oih5lhC-E,43
36
+ fhe_cli-1.3.1.dist-info/top_level.txt,sha256=X1XW-ry2gtjQW9sgKA8Z9Q_5Q1_XTOb8AxVZCpTskVs,7
37
+ fhe_cli-1.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fhecli = fhecli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ravin THILAGARASA, Thomas GRUNDSZTEJN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ fhecli
fhecli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """fhecli : un atelier d'exercices en ligne de commande, à la rustlings,
2
+ pour apprendre le chiffrement classique et le chiffrement homomorphe (FHE).
3
+ """
4
+
5
+ __version__ = "1.3.1"
fhecli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Permet `python -m fhecli`."""
2
+
3
+ from fhecli.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
fhecli/cli.py ADDED
@@ -0,0 +1,220 @@
1
+ """Point d'entrée de la commande `fhecli`."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from fhecli import __version__, resources, ui
8
+ from fhecli.info import exercise_names, find_exercise, load_exercises
9
+ from fhecli.runner import PYTEST_SUBCOMMAND, has_marker, run_tests, status
10
+ from fhecli.watch import watch as watch_loop
11
+ from fhecli.workspace import DEFAULT_DIR, WorkspaceNotFound, find_workspace, init_workspace
12
+
13
+ # Codes de sortie
14
+ EXIT_OK = 0
15
+ EXIT_ERROR = 1 # tests en échec, exercice inconnu, pas d'espace de travail...
16
+ EXIT_MARKER_LEFT = 2 # tests OK mais « # I AM NOT DONE » encore présent
17
+
18
+ STATUS_ICON = {
19
+ "done": ui.success("[x]"),
20
+ "almost": ui.warn("[~]"),
21
+ "pending": ui.failure("[ ]"),
22
+ }
23
+
24
+
25
+ def _workspace() -> Path:
26
+ try:
27
+ return find_workspace()
28
+ except WorkspaceNotFound:
29
+ print(ui.failure("Aucun espace de travail fhecli trouvé dans ce dossier ni dans ses parents."))
30
+ print(f"Lancez d'abord {ui.bold('fhecli init')} (il crée le dossier ./{DEFAULT_DIR}),")
31
+ print("puis placez-vous dans ce dossier.")
32
+ raise SystemExit(EXIT_ERROR)
33
+
34
+
35
+ def _exercise_or_exit(name: str):
36
+ ex = find_exercise(_workspace(), name)
37
+ if ex is None:
38
+ print(ui.failure(f"Exercice inconnu : {name}"))
39
+ print(f"Utilisez {ui.bold('fhecli list')} pour voir les noms disponibles.")
40
+ raise SystemExit(EXIT_ERROR)
41
+ return ex
42
+
43
+
44
+ def cmd_init(args) -> int:
45
+ target = Path(args.directory)
46
+ try:
47
+ ws = init_workspace(target, exercise_names())
48
+ except FileExistsError:
49
+ print(ui.failure(f"Un espace de travail existe déjà dans {target.resolve()}."))
50
+ print("Rien n'a été modifié. Choisissez un autre dossier : fhecli init <dossier>")
51
+ return EXIT_ERROR
52
+ print(ui.success(f"Espace de travail créé : {ws}"))
53
+ print()
54
+ print("Pour commencer :")
55
+ print(f" cd {ws}")
56
+ print(" fhecli watch")
57
+ return EXIT_OK
58
+
59
+
60
+ def cmd_list(args) -> int:
61
+ exercises = load_exercises(_workspace())
62
+ print(ui.bold(f"{len(exercises)} exercices :"))
63
+ print()
64
+ last_category = None
65
+ done_count = 0
66
+ for ex in exercises:
67
+ if ex.category != last_category:
68
+ print(ui.info(ex.category))
69
+ last_category = ex.category
70
+ st = status(ex.path)
71
+ if st == "done":
72
+ done_count += 1
73
+ bonus_tag = ui.warn(" (bonus)") if ex.bonus else ""
74
+ print(f" {STATUS_ICON[st]} {ex.name:<24} {ex.title}{bonus_tag}")
75
+ print()
76
+ print(f"Progression : {done_count}/{len(exercises)} exercices résolus.")
77
+ print(ui.DIM + "[x] résolu [~] tests OK, marqueur à retirer [ ] pas encore bon" + ui.RESET)
78
+ return EXIT_OK
79
+
80
+
81
+ def cmd_run(args) -> int:
82
+ ex = _exercise_or_exit(args.name)
83
+ print(ui.bold(f"{ex.name} - {ex.title}"))
84
+ print(f"Fichier : {ex.path}")
85
+ print()
86
+ passed, output = run_tests(ex.path)
87
+ print(output)
88
+ print()
89
+ if passed and has_marker(ex.path):
90
+ print(ui.warn(
91
+ "Tests OK, mais le fichier contient encore « # I AM NOT DONE ». "
92
+ "Retirez cette ligne une fois satisfait de votre solution."
93
+ ))
94
+ return EXIT_MARKER_LEFT
95
+ if passed:
96
+ print(ui.success("Tous les tests passent."))
97
+ return EXIT_OK
98
+ print(ui.failure("Des tests échouent encore."))
99
+ print(f"Besoin d'un coup de pouce ? {ui.bold('fhecli hint ' + ex.name)}")
100
+ return EXIT_ERROR
101
+
102
+
103
+ def cmd_hint(args) -> int:
104
+ ex = _exercise_or_exit(args.name)
105
+ print(ui.bold(f"Indice pour {ex.name} :"))
106
+ print(ex.hint or "Pas d'indice supplémentaire : relisez le docstring en haut du fichier.")
107
+ return EXIT_OK
108
+
109
+
110
+ def cmd_solution(args) -> int:
111
+ ex = _exercise_or_exit(args.name)
112
+ if not resources.has_solution(ex.name):
113
+ print(ui.failure("Pas de solution disponible pour cet exercice."))
114
+ return EXIT_ERROR
115
+ if status(ex.path) != "done":
116
+ print(ui.warn(f"La solution de {ex.name} est verrouillée."))
117
+ print("Elle se débloque quand l'exercice est résolu : les tests passent ET la")
118
+ print("ligne « # I AM NOT DONE » a été retirée du fichier.")
119
+ print(f"Vérifiez où vous en êtes avec {ui.bold('fhecli run ' + ex.name)}, "
120
+ f"ou demandez un indice avec {ui.bold('fhecli hint ' + ex.name)}.")
121
+ return EXIT_ERROR
122
+ print(ui.bold(f"Solution de référence pour {ex.name} :"))
123
+ print(ui.rule())
124
+ print(resources.solution_text(ex.name))
125
+ print(ui.rule())
126
+ return EXIT_OK
127
+
128
+
129
+ def cmd_reset(args) -> int:
130
+ ws = _workspace()
131
+ if args.all:
132
+ targets = load_exercises(ws)
133
+ question = (f"Remettre les {len(targets)} exercices à leur énoncé de départ ? "
134
+ "Tout votre code sera perdu.")
135
+ elif args.name:
136
+ targets = [_exercise_or_exit(args.name)]
137
+ question = f"Remettre {targets[0].name} à son énoncé de départ ? Votre code sera perdu."
138
+ else:
139
+ print(ui.failure("Précisez un nom d'exercice, ou utilisez --all."))
140
+ return EXIT_ERROR
141
+
142
+ if not args.yes and not ui.confirm(question):
143
+ print("Annulé, rien n'a été modifié.")
144
+ return EXIT_OK
145
+
146
+ for ex in targets:
147
+ ex.path.parent.mkdir(parents=True, exist_ok=True)
148
+ ex.path.write_text(resources.original_text(ex.name), encoding="utf-8")
149
+ if args.all:
150
+ print(ui.success(f"Les {len(targets)} exercices ont été réinitialisés."))
151
+ else:
152
+ print(ui.success(f"{targets[0].name} a été réinitialisé à son énoncé de départ."))
153
+ return EXIT_OK
154
+
155
+
156
+ def cmd_watch(args) -> int:
157
+ watch_loop(load_exercises(_workspace()))
158
+ return EXIT_OK
159
+
160
+
161
+ def build_parser() -> argparse.ArgumentParser:
162
+ parser = argparse.ArgumentParser(
163
+ prog="fhecli",
164
+ description=(
165
+ "Atelier d'exercices en ligne de commande : chiffrement classique "
166
+ "et chiffrement homomorphe (FHE). Inspiré de rustlings."
167
+ ),
168
+ epilog="Pour commencer : fhecli init, puis cd fhe-exercices et fhecli watch.",
169
+ )
170
+ parser.add_argument("--version", action="version", version=f"fhecli {__version__}")
171
+ sub = parser.add_subparsers(dest="command", required=True, metavar="commande")
172
+
173
+ p_init = sub.add_parser("init", help="Crée un espace de travail avec les exercices")
174
+ p_init.add_argument("directory", nargs="?", default=DEFAULT_DIR,
175
+ help=f"Dossier à créer (défaut : {DEFAULT_DIR})")
176
+ p_init.set_defaults(func=cmd_init)
177
+
178
+ p_watch = sub.add_parser("watch", help="Mode guidé : avance exercice par exercice")
179
+ p_watch.set_defaults(func=cmd_watch)
180
+
181
+ p_list = sub.add_parser("list", help="Liste tous les exercices et leur statut")
182
+ p_list.set_defaults(func=cmd_list)
183
+
184
+ p_run = sub.add_parser("run", help="Lance les tests d'un seul exercice")
185
+ p_run.add_argument("name", help="Nom de l'exercice (ex : ex1_cesar)")
186
+ p_run.set_defaults(func=cmd_run)
187
+
188
+ p_hint = sub.add_parser("hint", help="Affiche un indice pour un exercice")
189
+ p_hint.add_argument("name")
190
+ p_hint.set_defaults(func=cmd_hint)
191
+
192
+ p_solution = sub.add_parser("solution", help="Affiche la solution (une fois l'exercice résolu)")
193
+ p_solution.add_argument("name")
194
+ p_solution.set_defaults(func=cmd_solution)
195
+
196
+ p_reset = sub.add_parser("reset", help="Remet un exercice à son énoncé de départ")
197
+ p_reset.add_argument("name", nargs="?", help="Nom de l'exercice (à omettre avec --all)")
198
+ p_reset.add_argument("--all", action="store_true", help="Réinitialise tous les exercices")
199
+ p_reset.add_argument("-y", "--yes", action="store_true", help="Ne pas demander de confirmation")
200
+ p_reset.set_defaults(func=cmd_reset)
201
+
202
+ return parser
203
+
204
+
205
+ def main(argv: list[str] | None = None) -> int:
206
+ ui.setup_console()
207
+ argv = sys.argv[1:] if argv is None else argv
208
+ if argv and argv[0] == PYTEST_SUBCOMMAND:
209
+ # Usage interne (exécutable autonome) : `fhecli _run-pytest <args>`
210
+ # exécute le pytest embarqué. Absent de l'aide.
211
+ import pytest
212
+
213
+ return int(pytest.main(argv[1:]))
214
+ parser = build_parser()
215
+ args = parser.parse_args(argv)
216
+ return args.func(args)
217
+
218
+
219
+ if __name__ == "__main__":
220
+ sys.exit(main())
fhecli/data/info.toml ADDED
@@ -0,0 +1,77 @@
1
+ # Liste des exercices, dans l'ordre pédagogique suivi par `fhecli watch`.
2
+ #
3
+ # Parcours : du chiffrement classique (où tout calcul sur le chiffré est
4
+ # impossible) jusqu'au chiffrement homomorphe, en reconstruisant Paillier à la
5
+ # main, puis un score bancaire chiffré de bout en bout, et enfin un vrai calcul
6
+ # FHE avec CKKS (bonus).
7
+
8
+ [[exercises]]
9
+ name = "ex1_cesar"
10
+ category = "A. Chiffrement classique"
11
+ title = "Le chiffrement de César"
12
+ difficulty = "facile"
13
+ hint = "Pour tester si `c` est une lettre, utilisez 'a' <= c <= 'z' (et 'A' <= c <= 'Z'), pas c.islower() / c.isupper() : ces méthodes renvoient aussi True pour les lettres accentuées comme « é » ou « à », qui doivent rester inchangées. La nouvelle position est (position + shift) % 26."
14
+
15
+ [[exercises]]
16
+ name = "ex2_xor_malleability"
17
+ category = "A. Chiffrement classique"
18
+ title = "XOR et le danger de la malléabilité"
19
+ difficulty = "facile"
20
+ hint = "reveal_xor_of_plaintexts ne fait qu'une seule chose : XOR de c1 et c2 entre eux (réutilisez xor_bytes). C'est le XOR qui annule la clé, pas une astuce compliquée."
21
+
22
+ [[exercises]]
23
+ name = "ex3_fernet_tamper"
24
+ category = "A. Chiffrement classique"
25
+ title = "Chiffrement authentifié avec Fernet"
26
+ difficulty = "facile"
27
+ hint = "bytearray(token) permet de modifier un octet d'un objet bytes immuable : data[0] ^= 0xFF. Attrapez cryptography.fernet.InvalidToken avec un try/except."
28
+
29
+ [[exercises]]
30
+ name = "ex4_modpow"
31
+ category = "B. Briques mathématiques"
32
+ title = "Exponentiation modulaire rapide"
33
+ difficulty = "facile"
34
+ hint = "À chaque tour : si le bit de poids faible de exp vaut 1 (exp & 1), multipliez result par base modulo mod ; puis élevez base au carré modulo mod et faites exp //= 2. Ne calculez jamais base ** exp directement."
35
+
36
+ [[exercises]]
37
+ name = "ex5_modinv"
38
+ category = "B. Briques mathématiques"
39
+ title = "PGCD étendu et inverse modulaire"
40
+ difficulty = "moyen"
41
+ hint = "extended_gcd est récursif : cas de base b == 0 -> (a, 1, 0) ; sinon g, x1, y1 = extended_gcd(b, a % b) puis renvoyez (g, y1, x1 - (a // b) * y1). Dans mod_inverse, si g != 1 levez ValueError, sinon renvoyez x % mod (le % gère les x négatifs)."
42
+
43
+ [[exercises]]
44
+ name = "ex6_paillier_keygen"
45
+ category = "C. Paillier, pas à pas"
46
+ title = "Générer une paire de clés Paillier"
47
+ difficulty = "moyen"
48
+ hint = "n = p*q, g = n+1, lam = (p-1)*(q-1), et mu = _mod_inverse(lam, n) (la fonction est déjà fournie). Renvoyez public_key = {\"n\": n, \"g\": g, \"n2\": n*n} et private_key = {\"lam\": lam, \"mu\": mu}."
49
+
50
+ [[exercises]]
51
+ name = "ex7_paillier_enc_dec"
52
+ category = "C. Paillier, pas à pas"
53
+ title = "Chiffrer et déchiffrer avec Paillier"
54
+ difficulty = "moyen"
55
+ hint = "encrypt : ((1 + m*n) % n2) * pow(r, n, n2) % n2 (pensez à m = m % n). decrypt : u = pow(c, lam, n2), puis m = (_l(u, n) * mu) % n, et enfin si m > n // 2 : m -= n (représentation symétrique pour les négatifs)."
56
+
57
+ [[exercises]]
58
+ name = "ex8_paillier_homomorphic"
59
+ category = "C. Paillier, pas à pas"
60
+ title = "Les propriétés homomorphes de Paillier"
61
+ difficulty = "moyen"
62
+ hint = "add_encrypted : (c1 * c2) % n2 (multiplier les chiffrés = additionner les clairs). mul_by_scalar : pow(c, k, n2) si k >= 0 ; si k < 0, pow(_mod_inverse(c, n2), -k, n2)."
63
+
64
+ [[exercises]]
65
+ name = "ex9_scoring_paillier"
66
+ category = "D. Scoring chiffré"
67
+ title = "Un score bancaire chiffré de bout en bout"
68
+ difficulty = "difficile"
69
+ hint = "Chiffrez chaque feature avec encrypt(pub, x, _random_r(pub[\"n\"])), multipliez chaque chiffré par son poids avec mul_by_scalar, partez de total = encrypt(pub, bias, 1), additionnez tous les termes avec add_encrypted, et ne déchiffrez qu'à la toute fin."
70
+
71
+ [[exercises]]
72
+ name = "ex10_ckks_bonus"
73
+ category = "E. Bonus : FHE réel avec CKKS"
74
+ title = "Votre premier calcul FHE avec CKKS (TenSEAL)"
75
+ difficulty = "bonus"
76
+ bonus = true
77
+ hint = "Trois lignes : enc_vector = ts.ckks_vector(context, values) ; enc_result = enc_vector.dot(weights) ; return enc_result.decrypt()[0]. Ce bonus nécessite TenSEAL : pip install \"fhe-cli[ckks]\"."