- Rust 85.3%
- HTML 5.9%
- Python 4.8%
- CSS 2.8%
- Dockerfile 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| crossgen | ||
| site | ||
| .dockerignore | ||
| .env.exemple | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| merge_definitions.py | ||
| new_def.csv | ||
| README.md | ||
| script_collecte.py | ||
Mots fléchés du jour
Générateur de mots fléchés à la française en Rust, et le site qui en publie une grille nouvelle chaque jour. Tout est écrit en Rust, y compris l'interface : le navigateur exécute du WebAssembly, il n'y a pas une ligne de JavaScript écrite à la main.
Les grilles des joueurs sont chiffrées de bout en bout. Le serveur enregistre des octets dont il ne possède aucune clé : il ne peut pas lire une lettre saisie, ni savoir si une grille est terminée.
| Dossier | Rôle |
|---|---|
crossgen/ |
le générateur : bibliothèque et exécutable en ligne de commande |
site/ |
le site : serveur Axum, rendu Leptos, base PostgreSQL, WebAssembly |
cargo test --release # les deux crates
cargo leptos watch # le site sur http://127.0.0.1:3000
cargo leptos s'installe avec cargo install cargo-leptos --locked, et la
cible WebAssembly avec rustup target add wasm32-unknown-unknown.
Le site et ses tests parlent à un PostgreSQL déjà lancé, qu'ils ne provisionnent
pas — ils appliquent seulement les migrations. Les identifiants vivent dans un
.env à la racine, lu au démarrage du serveur comme au lancement des tests :
cp .env.exemple .env # puis renseigner DATABASE_URL et TEST_DATABASE_URL
.env est ignoré par Git. Ce qui est déjà exporté dans l'environnement l'emporte
sur son contenu, et son absence n'est pas une erreur : en conteneur, les
variables viennent de l'hôte.
Le générateur
cargo run -p crossgen --release
La grille par défaut fait 11 × 16 : elle est entièrement remplie de lettres, et
les cases grises portent une ou deux définitions avec une flèche indiquant où
commence la réponse. Quatre fichiers sont écrits dans out/ — grille.txt,
grille.html, grille.svg, grille.json — et la grille est affichée dans le
terminal.
Règles respectées
Le solveur place les cases noires au fil du remplissage, sans symétrie imposée, et garantit que :
- toute suite d'au moins deux lettres est un mot de la liste (les mots de deux lettres sont admis, comme dans les grilles françaises) ;
- aucune lettre n'est isolée : elle appartient toujours à un mot horizontal ou vertical ;
- chaque mot est porté par une case noire voisine, qui lui donne sa définition et sa flèche : celle qui le précède dans son propre sens (▶ vers la droite, ▼ vers le bas), ou, en coude, celle posée au-dessus d'un mot horizontal (⤷) ou à gauche d'un mot vertical (⤵). Un mot peut donc commencer sur le bord haut ou gauche pourvu qu'un coude puisse le définir ; seul un mot partant de la case (0, 0) reste impossible ;
- chaque case noire porte au moins une définition, au quota près (
--max-idle, 2 par défaut) ; - aucun mot n'apparaît deux fois (sauf
--allow-repeats).
La grille produite est revérifiée de zéro par Grid::violations avant d'être
écrite ou publiée : le solveur ne s'auto-certifie pas.
Liste de mots
Format : une entrée par ligne, MOT|définition courte. Les lignes vides et
celles commençant par # sont ignorées ; une ligne sans | reçoit une
définition de remplacement. Les accents, traits d'union et apostrophes sont
retirés pour la grille et conservés dans les définitions.
crossgen/data/fr.txt contient environ 1 100 entrées. La qualité des grilles
dépend directement de la taille de la liste : une liste plus fournie, passée
avec --words, donne des grilles plus denses, plus rapides à trouver, et avec
moins de mots de deux lettres.
Options
--help donne la liste complète. Les plus utiles :
| Option | Effet |
|---|---|
--seed N |
rend la grille reproductible ; la même graine redonne la même grille |
--width / --height |
dimensions de la grille |
--words <fichier> |
liste de mots à utiliser |
--format text,html,svg,json |
sorties à produire |
--max-idle N |
cases noires tolérées sans définition |
--max-run N |
longueur maximale d'un mot |
--timeout S |
temps accordé à la recherche |
Les deux réglages de recherche, --beam et --restart-nodes, sont réglés pour
data/fr.txt ; les élargir n'aide pas, la recherche procède par redémarrages
aléatoires plutôt que par exploration exhaustive.
| Fichier | Rôle |
|---|---|
crossgen/src/dict.rs |
lecture de la liste, normalisation, trie de préfixes |
crossgen/src/grid.rs |
cases, extraction des mots, vérification des règles |
crossgen/src/generator.rs |
remplissage par retour arrière, cases noires dynamiques |
crossgen/src/fleches.rs |
rattachement des définitions à leur case et à leur flèche |
crossgen/src/render/ |
sorties terminal, HTML, SVG, JSON |
Le site
Une grille par jour civil UTC, la même pour tout le monde. La graine dérive de
la date (sha256("crossgen/daily/v1" ‖ date ‖ tentative)), donc une journée
perdue se régénère à l'identique. Une tâche de fond prépare la grille du jour et
celle du lendemain, pour que personne n'attende le solveur à minuit.
La charge utile envoyée au navigateur contient les définitions et la forme de la
grille, jamais les réponses : chaque mot est accompagné d'une empreinte
sha256(date ‖ position ‖ réponse) qui permet de vérifier sa saisie hors ligne.
La solution complète n'est servie que pour les jours passés.
| Fichier | Rôle |
|---|---|
site/src/crypto.rs |
tout le chiffrement, exécuté dans le navigateur |
site/src/api.rs |
la frontière client/serveur, en fonctions Leptos |
site/src/components/grid.rs |
la grille jouable |
site/src/server/ |
base PostgreSQL, sessions, production des grilles |
site/migrations/ |
le schéma |
Chiffrement de bout en bout
mot de passe ──Argon2id(sel, 19 Mio, 2 passes)──▶ master
├─HKDF("cw-auth")─▶ auth_key ──▶ serveur
└─HKDF("cw-enc")──▶ enc_key (reste ici)
vault_key (32 octets aléatoires) ──scellée par enc_key───────▶ blob stocké
──scellée par la clé de secours──▶ blob stocké
progression du jour ──scellée par vault_key (XChaCha20-Poly1305)──▶ serveur
Le serveur ne voit jamais que auth_key, qu'il rehache en Argon2id avant de
la stocker : une base volée ne permet même pas de se connecter. Les deux clés
sortant du même master passent par des chemins HKDF distincts, donc auth_key
ne dit rien de enc_key.
Changer de mot de passe rescelle simplement vault_key : aucune grille n'est
rechiffrée et la clé de secours reste valable. À l'inscription, cette clé de
secours est affichée une seule fois ; elle est la seule autre façon d'ouvrir
le coffre.
Perdre le mot de passe et la clé de secours rend les données définitivement illisibles. C'est la conséquence directe de la promesse, pas un défaut : il n'existe aucune réinitialisation par courriel, puisqu'il n'y a rien à réinitialiser côté serveur.
Ce que le serveur voit malgré tout
Le chiffrement protège le contenu, pas l'existence des échanges. Restent
visibles : l'adresse électronique qui sert d'identifiant, les jours pour
lesquels un compte a enregistré quelque chose, la taille de chaque sauvegarde,
l'horodatage des requêtes et l'adresse IP. La page /confidentialite le dit
aux joueurs dans les mêmes termes.
La clé de coffre vit en mémoire dans l'onglet, avec un reflet dans le
sessionStorage pour survivre à un rechargement. C'est un compromis assumé,
documenté dans site/src/session.rs.
Configuration
| Variable | Défaut | Effet |
|---|---|---|
DATABASE_URL |
postgres://localhost/crosswords |
la base, qui doit déjà exister |
TEST_DATABASE_URL |
postgres://localhost/crosswords_test |
la base des tests, un schéma par test |
CW_SECURE_COOKIES |
absent | à définir derrière un vrai TLS, pour le drapeau Secure |
LEPTOS_SITE_ADDR |
127.0.0.1:3000 |
adresse d'écoute |
Vérification
cargo test --release # 94 tests, dont le parcours zéro accès
cargo leptos build && ./target/debug/site
cargo run -p site --features ssr --no-default-features --example parcours_complet
L'exemple parcours_complet joue le rôle du navigateur contre un serveur qui
tourne : inscription, sauvegarde, reconnexion depuis un autre client,
changement de mot de passe, récupération par clé de secours. Il vérifie au
passage qu'aucun secret ne part sur le réseau.
Le contrôle décisif tient en une ligne :
psql "$DATABASE_URL" -c "select ciphertext from progress"
Ce qui en sort doit être illisible.