No description
  • Rust 85.3%
  • HTML 5.9%
  • Python 4.8%
  • CSS 2.8%
  • Dockerfile 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-06 17:18:09 +01:00
crossgen chore(definitions): Add more word definitions 2026-11-06 17:18:09 +01:00
site build: Update dependencies 2026-11-05 01:13:35 +01:00
.dockerignore build: add Dockerfile 2026-07-28 12:11:09 +02:00
.env.exemple refactor: Optimize the server to reduce load 2026-07-28 17:11:21 +02:00
.gitignore fix(UI): Change the order of the infos in the archive menu 2026-08-07 18:33:22 +02:00
Cargo.lock build: Update dependencies 2026-11-05 01:13:35 +01:00
Cargo.toml refactor: Optimize the server to reduce load 2026-07-28 17:11:21 +02:00
Dockerfile refactor: Externalize JS and CSS for ecoindex 2026-11-04 23:56:04 +01:00
merge_definitions.py feat: Update collecting and agregation of definitions 2026-09-09 00:33:44 +02:00
new_def.csv feat: Update collecting and agregation of definitions 2026-09-09 00:33:44 +02:00
README.md feat: Add new type of arrow for the grid 2026-08-08 00:44:50 +02:00
script_collecte.py feat: Update collecting and agregation of definitions 2026-09-09 00:33:44 +02:00

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 :

  1. 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) ;
  2. aucune lettre n'est isolée : elle appartient toujours à un mot horizontal ou vertical ;
  3. 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 ;
  4. chaque case noire porte au moins une définition, au quota près (--max-idle, 2 par défaut) ;
  5. 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.