Spécifications : stockage cloud chiffré de bout en bout sur Telegram
Version révisée 0.2 (2026-10-27). Les changements par rapport à la version initiale sont listés en annexe A. Les formats binaires et les messages signés décrits ici sont normatifs : un client tiers doit les reproduire à l'octet près.
1. Vue d'ensemble
Le service stocke des fichiers chiffrés côté client dans des canaux Telegram, envoyés par des bots. Les métadonnées (utilisateurs, dossiers, pointeurs vers les fichiers, quotas, plans, partages) sont dans une base de données MySQL. L'API est servie par des Cloudflare Workers répartis sur plusieurs comptes « serveurs ». Un compte « maître » supervise leur charge.
Le système repose sur quatre principes :
- Le serveur ne voit jamais de clé privée, de mot de passe, de contenu en clair, ni de nom de fichier ou de dossier en clair.
- Le client chiffre, découpe et signe. Le serveur valide la forme, applique les quotas et contrôle l'accès.
- Toute donnée temporaire (morceaux non indexés) est purgée automatiquement.
- Le nombre de comptes serveurs est piloté par un simple fichier de configuration (
servers.json).
2. Architecture
| Composant | Rôle | Hébergement |
|---|---|---|
| Serveurs API (N) | Authentification, upload, téléchargement, dossiers, partages, publication du quota | Un Worker par compte Cloudflare serveur |
| Compte maître | Registre des serveurs, collecte des quotas, tableau de bord, API des serveurs disponibles | Worker drive-master et Durable Object SQLite, sur un compte dédié (dépôt 014-Drive-Worker-Master) |
| Base partagée | Toutes les métadonnées | MySQL HeatWave (Oracle Cloud), via Hyperdrive |
| KV local | Quota du serveur | Un namespace KV par compte serveur |
| Telegram | Morceaux chiffrés et manifestes | Pool de bots et de canaux |
| Application web | Client 100 % navigateur : chiffrement, interface, liens publics | Cloudflare Pages, compte dédié (dépôt 014-Drive-Worker-WEBCLIENT) : https://app-014drive.pages.dev |
| Documentation | Site de documentation et référence de l'API | Cloudflare Pages, compte dédié (dépôt 014-Drive-DOCS) : https://docs-014drive.pages.dev |
2.1 Base de données partagée
D1 étant lié à un compte, la base partagée est une base MySQL externe (MySQL HeatWave). Hyperdrive est lui aussi
lié à un compte : chaque compte serveur possède sa propre configuration Hyperdrive, qui pointe vers la même base.
Son identifiant figure dans servers.json.
Contraintes :
- Le cache de requêtes Hyperdrive doit être désactivé (
--caching-disabled) : sinon, une lecture qui suit une écriture peut renvoyer une valeur périmée. - Le pilote est
mysql2avecdisableEval: true(obligatoire sur Workers). - Une connexion est ouverte à la demande par requête et fermée après la réponse.
- Le schéma (section 12) est écrit pour MySQL 8.
Le registre des serveurs n'est pas dans cette base : il est tenu par le maître sur son propre compte (11.2), pour que le maître ne dépende pas de ce qu'il surveille.
2.2 Secrets et configuration
| Nom | Type | Portée | Usage |
|---|---|---|---|
TOKEN_HMAC_SECRET |
secret | partagé | Signature des jetons de session |
CHALLENGE_HMAC_SECRET |
secret | partagé | Signature des challenges sans état |
FAKE_SALT_SECRET |
secret | partagé | Faux sels et faux bundles (anti-énumération) |
BOT_TOKENS |
secret | partagé | JSON {bot_id: token} du pool de bots |
CF_ANALYTICS_TOKEN |
secret | par compte | Lecture des analytics du compte (« Account Analytics: Read ») |
CF_ACCOUNT_ID |
variable | par compte | Compte interrogé pour le quota |
SERVER_NAME |
variable | par compte | Nom du serveur (srv-01…) |
WEBAUTHN_RP_ID |
variable | partagé | Domaine de l'application web |
WEBAUTHN_ORIGINS |
variable | partagé | Origines autorisées, séparées par des virgules |
DAILY_REQUEST_LIMIT |
variable | partagé | Quota quotidien de requêtes (100 000 en gratuit) |
HYPERDRIVE |
binding | par compte | Accès à la base partagée |
USAGE_KV |
binding | par compte | Quota du serveur |
3. Modèle cryptographique
3.1 Algorithmes
| Usage | Algorithme |
|---|---|
| Dérivation depuis le mot de passe | Argon2id v1.3 (19), m = 64 Mio, t = 3, p = 1, sortie 32 octets |
| Dérivation de sous-clés | HKDF-SHA256 (sel vide sauf mention contraire, info = libellé ASCII) |
| Chiffrement symétrique | XChaCha20-Poly1305 IETF (nonce 24 octets, tag 16 octets) |
| Signature | Ed25519 |
| Chiffrement de clé pour un destinataire | X25519 en sealed box (libsodium crypto_box_seal) |
| Empreinte de nom (doublons) | HMAC-SHA256 |
| Déverrouillage par passkey | WebAuthn PRF, puis HKDF-SHA256 |
libsodium (wasm dans le navigateur) couvre tous les besoins côté client. Côté Worker : Ed25519 et HMAC via WebCrypto ; pour les passkeys, ECDSA P-256 et RSA (WebCrypto) et un décodeur CBOR léger.
3.2 Encodages
- Champs binaires dans les corps JSON : base64 standard (RFC 4648 §4), avec padding.
- Jetons et challenges : base64url sans padding.
- Identifiants : UUID v4 en minuscules (
xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), générés par le client pour les dossiers et fichiers, par le serveur pour les utilisateurs. - Dates : secondes Unix (entiers).
- Noms d'utilisateur :
^[a-z0-9][a-z0-9_.-]{2,31}$, envoyés en minuscules.
3.3 Identité de l'utilisateur et bundle de clés
Le mot de passe protège un bundle de clés aléatoires : changer de mot de passe ne demande ni de rechiffrer les fichiers ni de refaire les partages.
mot de passe + sel (16 octets) ──Argon2id──► KEK (32 octets)
Bundle en clair (97 octets) :
offset 0 1 version = 0x01
offset 1 32 sign_seed (graine Ed25519 ; sign_sk = keypair(sign_seed))
offset 33 32 box_sk (clé privée X25519)
offset 65 32 root_key
bundle_enc (137 octets) = nonce (24) ‖ XChaCha20-Poly1305(KEK, bundle, AAD = "TGC-bundle-v1") (97 + 16)
kdf_params est transmis sous la forme {"alg":"argon2id","v":19,"m":65536,"t":3,"p":1} (m en Kio). Le serveur
accepte m ∈ [65536, 1048576], t ∈ [3, 10], p ∈ [1, 4], et stocke la sérialisation dans cet ordre de clés.
Le serveur stocke le sel, les paramètres, bundle_enc et les clés publiques sign_pk et box_pk. La taille fixe de
bundle_enc permet de renvoyer des faux bundles indiscernables pour les comptes inexistants.
Comme bundle_enc est récupérable avant authentification, une attaque hors ligne par dictionnaire reste possible sur
un mot de passe faible. Le client impose au moins 12 caractères avec une estimation d'entropie (zxcvbn), ou propose
une phrase de passe générée.
3.4 Hiérarchie de clés
root_key (dans le bundle)
└── Clé du dossier racine (aléatoire) wrappée par root_key
└── Clé de sous-dossier (aléatoire) wrappée par la clé du dossier parent
└── FEK, clé de fichier (aléatoire) wrappée par la clé du dossier qui le contient
Toute clé wrappée a la forme nonce (24) ‖ ciphertext (32) ‖ tag (16) = 72 octets, avec en AAD
"TGC-wrap-v1" ‖ item_id (UUID ASCII de l'objet protégé), ce qui empêche de déplacer un blob d'un objet vers un autre.
Chaque clé de dossier K_d donne :
K_d_name = HKDF(K_d, info = "name-enc"): chiffre les noms des enfants (AAD = UUID ASCII de l'enfant) ;K_d_hmac = HKDF(K_d, info = "name-hmac"): calculename_hmac(section 8.1).
4. Authentification
4.1 Messages signés
Les messages sont des octets ; ‖ est la concaténation et \n l'octet 0x0A.
| Nom | Message |
|---|---|
| Inscription | "REGISTER-v1" ‖ utf8(username) ‖ sign_pk (32) ‖ box_pk (32) |
| Connexion | utf8("AUTH-v1\n" + api_origin + "\n" + challenge) |
| Mot de passe | utf8("PASSWORD-v1\n" + user_id + "\n" + timestamp + "\n" + b64(salt) + "\n" + kdf_params + "\n" + b64(bundle_enc)) |
| Lien public | utf8("LINK-v1\n" + api_origin + "\n" + challenge), signé par la clé dérivée du lien |
| Ajout de passkey | "PASSKEY-ADD-v1" ‖ utf8(user_id) ‖ SHA-256(credential_id) ‖ SHA-256(clé COSE) ‖ SHA-256(bundle_enc_pk) |
| Suppression de passkey | utf8("PASSKEY-DEL-v1\n" + passkey_id + "\n" + timestamp) |
| Réglages | utf8("SETTINGS-v1\n" + user_id + "\n" + timestamp + "\npassword_login_enabled=" + (0 ou 1)) |
| Code de récupération | utf8("RECOVERY-v1\n" + user_id + "\n" + timestamp + "\n" + b64(recovery_bundle_enc)) |
api_origin est l'origine du serveur appelé (https://srv-01.exemple.workers.dev), sans barre finale. Elle lie la
signature au serveur : une signature obtenue par un faux serveur n'est pas rejouable sur un vrai.
Les requêtes signées avec horodatage sont acceptées à ± 300 s.
4.2 Challenges sans état
challenge = base64url(
version (1) = 0x01
purpose (1) 1 = connexion, 2 = création de passkey, 3 = connexion par passkey, 4 = lien public
expires_at (8) secondes Unix, big-endian
subject_len (1) ‖ subject (≤ 64 octets UTF-8) username, user_id ou share_id selon l'usage
random (32)
mac (32) HMAC-SHA256(CHALLENGE_HMAC_SECRET, tout ce qui précède)
)
Validité 60 s. Tout serveur peut vérifier un challenge émis par un autre. À la consommation, SHA-256(challenge décodé)
est inséré dans used_challenges ; une clé dupliquée signale un rejeu. Les lignes expirées sont purgées par le cron.
4.3 Jetons de session
token = "v1." ‖ base64url(payload) ‖ "." ‖ base64url(mac)
payload = utf8(sub + "|" + exp + "|" + scope)
mac = HMAC-SHA256(TOKEN_HMAC_SECRET, "TOKEN-v1|" ‖ payload)
scope vaut user pour une session, ou share:<share_id> pour un jeton de lien public. Durée : 15 minutes.
Le jeton est transmis dans Authorization: Bearer <token> et accepté par tous les serveurs sans accès à la base.
4.4 Inscription
POST /v1/auth/register
{
"username": "alice",
"salt": "b64 (16)",
"kdf_params": {"alg":"argon2id","v":19,"m":65536,"t":3,"p":1},
"bundle_enc": "b64 (137)",
"sign_pk": "b64 (32)",
"box_pk": "b64 (32)",
"root_folder": {"folder_id": "uuid", "wrapped_key": "b64 (72)"},
"proof": "b64 (64)"
}
Le serveur vérifie la forme, la preuve (message d'inscription), l'unicité du nom, puis crée dans une transaction
l'utilisateur (avec un user_handle aléatoire de 16 octets pour WebAuthn), le dossier racine et l'abonnement au plan
par défaut. Réponse 201 {user_id, root_folder_id}. Erreurs : USERNAME_TAKEN, ID_CONFLICT, INVALID_PROOF.
La clé du dossier racine est wrappée par root_key avec l'AAD "TGC-wrap-v1" ‖ folder_id.
4.5 Connexion par mot de passe
| Étape | Requête | Description |
|---|---|---|
| 1 | POST /v1/auth/prelogin {username} |
Retourne salt, kdf_params, bundle_enc. Pour un nom inconnu, ou si la connexion par mot de passe est désactivée, renvoie des valeurs factices déterministes : HMAC(FAKE_SALT_SECRET, label ‖ 0x00 ‖ i ‖ 0x00 ‖ username) concaténés, avec les paramètres KDF par défaut. |
| 2 | Local | Le client dérive la KEK et déchiffre le bundle. Un échec du tag signifie un mauvais mot de passe ou un compte inexistant, sans distinction possible. |
| 3 | POST /v1/auth/challenge {username} |
Retourne {challenge, expires_at}. Aucune lecture en base : un challenge est émis pour n'importe quel nom. |
| 4 | POST /v1/auth/verify {challenge, signature} |
Vérifie le challenge (HMAC, usage, expiration), la signature avec sign_pk, le statut du compte, puis consomme le challenge. Erreur unique AUTH_FAILED pour un nom inconnu ou une mauvaise signature. |
| 5 | Réponse | {token, expires_at, user_id, root_folder_id} |
À l'expiration du jeton, le client refait les étapes 3 et 4 sans redemander le mot de passe, puisque le bundle reste en mémoire.
4.6 Changement de mot de passe
POST /v1/auth/password (session) avec {salt, kdf_params, bundle_enc, timestamp, signature}. Le client dérive une
nouvelle KEK avec un nouveau sel et rechiffre le même bundle. La signature porte sur le message « Mot de passe »
(4.1). Les clés, les fichiers et les copies de bundle par passkey ne changent pas. Réponse 204.
4.7 Connexion par passkey
4.7.1 Principe
L'extension PRF de WebAuthn (fondée sur hmac-secret de CTAP2) fait renvoyer par l'authentificateur, à chaque
assertion, prf_out = HMAC(secret_interne, sel). Ce secret ne quitte jamais l'authentificateur. Le client en dérive une
clé qui chiffre une copie du bundle :
prf_salt = SHA-256("TGC-PRF-bundle-v1") (constante globale versionnée)
K_pk = HKDF(prf_out, info = "TGC-passkey-bundle-v1")
bundle_enc_pk = nonce (24) ‖ XChaCha20-Poly1305(K_pk, bundle, AAD = user_id ‖ SHA-256(credential_id) ‖ prf_version)
Le bundle est le même (mêmes sign_sk, box_sk, root_key) ; chaque passkey a sa propre copie chiffrée. Le sel
constant permet une connexion sans saisie du nom d'utilisateur.
4.7.2 Paramètres WebAuthn
| Paramètre | Valeur | Raison |
|---|---|---|
rp.id |
WEBAUTHN_RP_ID = app-014drive.pages.dev (application web) |
Les serveurs API sont sur d'autres domaines ; seule l'origine du client compte |
residentKey |
required |
Passkey découvrable : connexion sans nom d'utilisateur ni allowCredentials |
userVerification |
required |
La passkey déverrouille toutes les clés : biométrie ou PIN obligatoire |
attestation |
none |
Inutile, et plus respectueux de la vie privée |
pubKeyCredParams |
ES256 (−7), EdDSA (−8), RS256 (−257) | Authentificateurs courants |
user.id |
user_handle (16 octets aléatoires) |
Aucune donnée identifiante |
extensions.prf |
{ eval: { first: prf_salt } } |
Indispensable |
Les challenges réutilisent le mécanisme de 4.2 (usages 2 et 3).
4.7.3 Ajout d'une passkey (utilisateur connecté)
| Étape | Requête | Description |
|---|---|---|
| 1 | POST /v1/auth/passkeys/options (session) |
Challenge, rp, user, excludeCredentials et paramètres de 4.7.2 |
| 2 | Local | navigator.credentials.create(...). Si prf.enabled est faux, refus expliqué à l'utilisateur. Si prf.results est absent, enchaîner un navigator.credentials.get() limité à la nouvelle passkey. |
| 3 | Local | Dérivation de K_pk, chiffrement du bundle, libellé chiffré avec HKDF(root_key, "passkey-label"), preuve signée "PASSKEY-ADD-v1" ‖ user_id ‖ SHA-256(credential_id) ‖ SHA-256(cose_public_key) ‖ SHA-256(bundle_enc_pk) |
| 4 | POST /v1/auth/passkeys {attestation, bundle_enc_pk, label_enc, proof} (session) |
Vérifie challenge, type = "webauthn.create", origine, rpIdHash, drapeaux UP et UV, extrait la clé COSE, vérifie proof avec sign_pk |
La preuve signée empêche qu'un jeton de session volé suffise à enregistrer la passkey d'un attaquant.
4.7.4 Connexion
| Étape | Requête | Description |
|---|---|---|
| 1 | POST /v1/auth/passkey/challenge |
Challenge sans nom d'utilisateur (usage 3) |
| 2 | Local | navigator.credentials.get({ publicKey: { challenge, rpId, userVerification: "required", extensions: { prf: { eval: { first: prf_salt } } } }, mediation: "conditional" }) |
| 3 | POST /v1/auth/passkey/verify {challenge, credential_id, authenticatorData, clientDataJSON, signature, userHandle} |
Recherche par SHA-256(credential_id), contrôle de userHandle, du challenge, de type = "webauthn.get", de l'origine, du rpIdHash, de UP et UV, de la signature COSE et du compteur (si ancien et nouveau sont non nuls et que le nouveau n'augmente pas : suspicion de clonage) |
| 4 | Réponse | Jeton de session, bundle_enc_pk, sign_pk, box_pk, root_folder_id |
| 5 | Local | Déchiffrement du bundle, puis contrôle que la clé publique dérivée de sign_sk est bien sign_pk |
Le renouvellement du jeton se fait ensuite par le challenge Ed25519 de 4.5, sans nouveau geste biométrique.
4.7.5 Sécurité
bundle_enc_pk n'est renvoyé qu'après une assertion valide : il n'est pas exposé à l'attaque hors ligne de 3.3, et
même en cas de fuite de la base il est chiffré par une clé de 256 bits non dérivée d'un secret humain.
Option « passkey uniquement » : users.password_login_enabled = 0. prelogin renvoie alors des valeurs factices.
Le client ne l'autorise qu'avec au moins deux passkeys ou un code de récupération : 128 bits aléatoires montrés une
seule fois, qui chiffrent une copie supplémentaire du bundle (HKDF(code, "recovery-bundle-v1"), stockée dans
users.recovery_bundle_enc).
À faire figurer dans la documentation utilisateur :
- Une passkey synchronisée (iCloud, Google, gestionnaire de mots de passe) vaut ce que vaut la sécurité de ce compte. Une clé matérielle n'a pas ce défaut.
- Supprimer une passkey coupe l'accès pour l'avenir ; si elle a été compromise, seule une rotation complète des clés protège les données, et elle n'est pas prévue dans cette version.
- PRF n'est pas pris en charge partout (mode hybride par QR code, anciennes versions de Windows Hello). Le mot de passe reste la méthode universelle. Une matrice de compatibilité testée doit être publiée.
4.7.6 Passkeys sans PRF et appareils de confiance
Certains gestionnaires (Bitwarden, quand il stocke la passkey) ne fournissent pas PRF : la passkey authentifie mais ne donne aucun secret, elle ne peut donc pas déverrouiller les clés seule. Elle est acceptée sur un appareil de confiance :
POST /v1/auth/devices(session) crée un appareil et renvoiedevice_key(32 octets aléatoires gardés par le serveur) ;- le client chiffre le bundle par
HKDF(device_key, "TGC-device-v1")(AAD"TGC-device-v1" ‖ user_id ‖ device_id), puis par une clé AES-GCM locale non exportable (IndexedDB) ; - une passkey sans PRF est enregistrée sans
bundle_enc_pk(prf = 0, preuve calculée avecSHA-256d'une chaîne vide) ; sa connexion renvoiebundle_enc_pk: null, puis la session obtientdevice_keyparPOST /v1/auth/devices/{id}/unlocket le client ouvre ses deux couches ; DELETE /v1/auth/devices/{id}révoque l'appareil : sa copie locale devient inutilisable.
Ni une copie du profil du navigateur (il manque device_key), ni le serveur (il manque la clé locale) ne suffisent à
ouvrir le bundle. Les passkeys sans PRF ne comptent pas comme moyen de récupération (mode « passkey uniquement »,
protection de la dernière passkey).
4.7.7 Format des échanges WebAuthn
Les réponses WebAuthn sont transmises au format JSON standard (PublicKeyCredential.toJSON()), champs binaires en
base64url : {"credential": {"id", "response": {"clientDataJSON", "attestationObject"}}} à l'ajout,
{"credential": {"id", "response": {"clientDataJSON", "authenticatorData", "signature", "userHandle"}}} à la connexion.
Le serveur ne vérifie pas l'attestation (demandée à none) : il lit seulement authData. Il vérifie lui-même les
signatures ES256, EdDSA et RS256 via WebCrypto, avec un décodeur CBOR minimal, sans bibliothèque WebAuthn (celles-ci
embarquent un analyseur X.509 inutile ici et coûteux au démarrage).
Code de récupération : PUT /v1/auth/recovery {recovery_bundle_enc, timestamp, signature} (session), et
POST /v1/auth/recovery/prelogin {username} (sans authentification) qui renvoie la copie de récupération, ou une valeur
factice de même taille. Le client la déchiffre avec la clé dérivée du code, puis se connecte par challenge Ed25519.
4.7.8 Gestion
| Requête | Description |
|---|---|
GET /v1/auth/passkeys |
Identifiant, libellé chiffré, dates de création et de dernière utilisation, drapeaux BE et BS |
DELETE /v1/auth/passkeys/{id} |
Signature sign_sk de "PASSKEY-DEL-v1" ‖ id ‖ timestamp. Refusée s'il s'agit du dernier moyen de connexion. |
PATCH /v1/auth/settings |
{password_login_enabled}, signé par sign_sk, avec les contrôles de 4.7.5 |
5. Format des données
5.1 Morceau chiffré
Taille maximale : 19 Mio (19 922 944 octets), en-tête et tag compris, sous la limite de 20 Mo de getFile.
offset taille champ
0 4 magic "TGC1"
4 1 version = 0x01
5 24 nonce
29 n ciphertext
29+n 16 tag Poly1305
AAD = file_id (16 octets binaires de l'UUID) ‖ chunk_index (uint32 big-endian)
Le serveur valide le magic, la version et les bornes : au moins 46 octets, au plus 19 Mio, et tous les morceaux sauf le dernier exactement à la taille maximale. Il ne calcule pas de hash (hacher 19 Mio dépasserait 10 ms de CPU) ; l'intégrité est garantie par le tag AEAD, vérifié par le client.
5.2 Index (fourni par le client)
{
"v": 1,
"file_id": "uuid",
"upload_id": "uuid",
"chunk_count": 53,
"chunks": [ { "i": 0, "size": 19922944 } ],
"total_size": 1048576000,
"meta_enc": "b64(nonce ‖ ct ‖ tag)",
"author_pk": "b64",
"sig": "b64"
}
meta_enc est chiffré avec la FEK (AAD = "TGC-meta-v1" ‖ file_id) et contient le nom réel, le type MIME, la taille
en clair et les dates. sig est une signature Ed25519 sur la sérialisation canonique : JSON avec clés triées
récursivement, sans espaces, sans le champ sig (RFC 8785, JCS).
5.3 Manifeste (construit par le serveur)
{ "v": 1, "index": { …index client intact, signature comprise… }, "replicas": [ [ {bot_id, chat_id, message_id, file_id}, {…} ], … ] }
replicas[i] liste les copies du morceau i. Le manifeste est envoyé sur Telegram ; la base ne garde que ses propres
répliques (files.manifest_refs). Le serveur le garde 10 minutes en cache dans l'isolate. Un file_id Telegram n'est utilisable que par le bot qui l'a obtenu : bot_id est
toujours stocké avec chaque réplique.
6. Flux d'upload
| Étape | Requête | Traitement serveur |
|---|---|---|
| 1 | POST /v1/uploads {folder_id, chunk_count, total_size} |
Droits d'écriture sur le dossier, plan actif, quota en octets et en fichiers en tenant compte des réservations en cours (ligne du propriétaire verrouillée). chunk_count ≤ 1000 et total_size doit être cohérent : la taille de chaque morceau est fixée dès cette étape. Crée la session (upload_id, file_id généré par le serveur, expiration 24 h) et réserve le quota. Réponse {upload_id, file_id, expires_at}. |
| 2 | PUT /v1/uploads/{upload_id}/chunks/{i} (corps binaire) |
Valide le format (5.1), tire au hasard deux canaux actifs distincts du pool, envoie le morceau en flux avec sendDocument, écrit les deux répliques dans pending_chunks. Gère les 429 de Telegram (retry_after, relayé au client en Retry-After). Renvoyer un morceau déjà présent le remplace ; l'ancien est supprimé. |
| 3 | POST /v1/uploads/{upload_id}/commit {index, wrapped_fek, name_enc, name_hmac} |
Valide l'index (6.1). Si valide : manifeste envoyé sur Telegram, insertion dans files et names, suppression des pending_chunks, conversion de la réservation en consommation. Sinon : 422 INVALID_INDEX, session annulée et morceaux supprimés. Un nom déjà pris donne 409 NAME_EXISTS sans annuler la session : le client peut recommencer avec un autre nom. |
| 4 | DELETE /v1/uploads/{upload_id} |
Annulation volontaire, même purge. |
Emplacements de stockage. Un emplacement est un couple (bot, chat) enregistré dans channels. Deux sortes de
chats sont acceptées :
- un chat privé avec un utilisateur (identifiant positif) : chaque bot y a sa propre conversation, donc chaque couple (bot, chat) est un emplacement distinct. L'utilisateur doit avoir démarré chaque bot. Limite : un bot ne peut supprimer ses messages d'un chat privé que pendant 48 h. Au-delà, les messages d'un fichier supprimé restent dans le chat (chiffrés, donc illisibles) ;
- un canal (identifiant
-100…) où le bot est administrateur avec les droits de publier et de supprimer : tous les bots du canal partagent son historique, donc l'emplacement est le canal. Pas de limite de 48 h pour la suppression.
Les deux répliques d'un morceau vont toujours dans deux emplacements distincts.
Jeton d'upload et limite de CPU. POST /v1/uploads renvoie aussi upload_token :
"u1." ‖ base64url(upload_id|file_id|user_id|chunk_count|total_size|exp) ‖ "." ‖ base64url(HMAC-SHA256(TOKEN_HMAC_SECRET, "UPLOAD-v1|" ‖ payload)).
Présenté dans l'en-tête X-Upload-Token, il permet de valider un morceau sans lire la base : l'envoi d'un morceau ne
fait qu'une écriture SQL. Le corps du morceau n'est jamais traité en JavaScript : seul l'en-tête TGC1 est lu, puis le
flux est relayé nativement vers les deux répliques (tee(), FixedLengthStream), ce qui garde chaque requête sous la
limite de CPU du plan gratuit.
Envoi parallèle. Le client envoie jusqu'à 5 morceaux à la fois. Le serveur choisit les répliques du morceau i par
rotation (i × 2, i × 2 + 1 dans le pool trié) plutôt qu'au hasard : des morceaux simultanés tombent sur des bots
différents.
Limite de débit Telegram : un bot ne peut envoyer qu'environ 20 messages par minute dans un même canal. Un fichier de 1 Go représente 106 messages (53 morceaux × 2 répliques). Le pool doit compter assez de canaux pour que le tirage aléatoire répartisse la charge ; en cas de 429, le serveur réessaie sur un autre canal avant de renvoyer l'erreur.
6.1 Validation de l'index
L'index est valide si : upload_id et file_id correspondent à la session ; chunk_count est égal au nombre de
morceaux reçus, indices de 0 à n−1 sans trou ; chaque size correspond à la taille reçue ; la somme est égale à
total_size et respecte la réservation ; author_pk est la clé de l'utilisateur authentifié ; sig est valide ;
name_hmac n'existe pas déjà dans le dossier de destination.
6.2 Suppressions Telegram et purge des orphelins
Toutes les suppressions de messages Telegram (upload annulé ou expiré, morceau remplacé, fichier supprimé, réplique
orpheline après un échec partiel) passent par la table tg_trash, remplie dans la transaction qui déréférence les
messages. Une requête utilisateur ne fait donc jamais d'appel de suppression, quelle que soit la taille du fichier.
À chaque passage, le cron prend un lot de tg_trash (FOR UPDATE SKIP LOCKED, pour que plusieurs serveurs travaillent en
parallèle sans conflit), regroupe les messages par (bot, canal), et appelle deleteMessages (100 messages au plus par
appel, 40 appels au plus par passage). Les bots administrateurs avec le droit de suppression ne sont pas limités à 48 h.
Les erreurs 4xx autres que 429 abandonnent le lot ; les 429 et 5xx le laissent pour le passage suivant.
Les suppressions de fichiers et de dossiers ne touchent pas Telegram pendant la requête : chaque fichier supprimé
passe dans manifest_trash avec les références de son manifeste. À chaque passage, le cron en développe quelques-uns
(télécharge le manifeste, met ses répliques et celles des morceaux dans tg_trash). Si Telegram est indisponible, la
ligne est retentée ; au-delà de 7 jours, seul le manifeste est supprimé. Budget d'un passage : quota (2 sous-requêtes),
manifestes (16 au pire), suppressions (30).
Un passage sur trois, le cron sélectionne aussi les sessions expirées non validées (même verrouillage), met leurs
morceaux dans tg_trash et supprime les sessions, ce qui libère la réservation.
7. Flux de téléchargement
| Étape | Requête | Traitement serveur |
|---|---|---|
| 1 | GET /v1/files/{id} |
Contrôle d'accès : propriétaire, partage actif ou jeton de lien public. Retourne l'index client (sans références Telegram), wrapped_fek et name_enc. |
| 2 | GET /v1/files/{id}/chunks/{i} |
Même contrôle. getFile puis téléchargement de la réplique principale, renvoyée en flux. Bascule sur la seconde en cas d'échec. |
| 3 | Local | Vérification de sig, déchiffrement de la FEK, puis de chaque morceau (AEAD avec AAD), réassemblage. |
Une requête par morceau garantit qu'on reste sous la limite de sous-requêtes. Le jeton du bot ne sort jamais du serveur.
8. Dossiers
8.1 Noms chiffrés et doublons
name_enc = XChaCha20-Poly1305(K_parent_name, nom, AAD = UUID de l'élément), nonce aléatoire.
name_hmac = HMAC-SHA256(K_parent_hmac, lower(NFC(nom))) : les noms sont insensibles à la casse.
La table names (clé primaire (parent_id, name_hmac)), commune aux fichiers et aux dossiers, détecte les doublons
sans révéler les noms.
8.2 Opérations
| Requête | Description |
|---|---|
POST /v1/folders |
{parent_id, folder_id, wrapped_key, name_enc, name_hmac} |
GET /v1/folders/{id} |
Enfants : identifiants, clés wrappées, noms chiffrés, tailles |
PATCH /v1/folders/{id} |
Renommage : nouveaux name_enc et name_hmac |
POST /v1/folders/{id}/move |
{new_parent_id, wrapped_key, name_enc, name_hmac} |
PATCH /v1/files/{id} |
Renommage d'un fichier |
POST /v1/files/{id}/move |
{new_folder_id, wrapped_fek, name_enc, name_hmac} |
DELETE /v1/files/{id} |
Supprime le fichier, son nom et ses partages directs, libère le quota, et met le fichier dans manifest_trash |
DELETE /v1/folders/{id} |
Suppression récursive en base, instantanée (5000 éléments au plus, sinon 413 FOLDER_TOO_LARGE) : uploads en cours annulés, partages supprimés, fichiers mis dans manifest_trash, quota libéré |
8.3 Déplacement
Le client rechiffre la clé de l'élément avec la clé du nouveau parent, rechiffre le nom et recalcule le HMAC. Le serveur
met à jour l'enregistrement et names dans une transaction. Aucun morceau ni manifeste n'est modifié. Déplacer un
dossier emmène toute sa sous-arborescence.
Le serveur vérifie les droits d'écriture sur l'origine et la destination, rejette le déplacement d'un dossier dans l'un
de ses descendants (400 MOVE_INTO_DESCENDANT), et, si la destination appartient à un autre utilisateur, transfère le
quota et la propriété de toute la sous-arborescence après avoir vérifié la place disponible. Les partages émis par
l'ancien propriétaire sur ces éléments sont révoqués (disabled_reason = 'moved'). Le dossier racine ne peut être ni
renommé, ni déplacé, ni supprimé (400 ROOT_FOLDER).
Sortir un fichier d'un dossier partagé coupe l'accès côté serveur, mais un destinataire qui a déjà obtenu la FEK peut toujours lire ce qu'il a téléchargé. Le client propose l'option « rechiffrer » (nouvelle FEK et nouvel upload). Quand un élément entre dans un dossier partagé, le client indique avec combien de personnes il est partagé.
9. Partage
9.1 Avec un utilisateur inscrit
| Étape | Description |
|---|---|
| 1 | GET /v1/users/{username}/keys renvoie box_pk et sign_pk. Le client affiche une empreinte courte à comparer avec le destinataire, et mémorise la clé (confiance au premier usage). |
| 2 | Le client chiffre la FEK ou la clé du dossier en sealed box pour box_pk, ainsi que le nom de la ressource. |
| 3 | POST /v1/shares {resource_type, resource_id, recipient_username, wrapped_key, name_enc, permissions, expires_at} |
| 4 | Le destinataire liste ses partages (GET /v1/shares/incoming) et ouvre la clé avec box_sk. |
Le partage de dossier est « vivant » : les éléments ajoutés ensuite sont accessibles.
9.2 Par lien public
https://<app>/s/{share_id}#{link_secret}
Le fragment n'est jamais envoyé au serveur. link_secret fait 32 octets aléatoires.
K_link = HKDF(link_secret, "link-enc") chiffre la FEK ou la clé du dossier ; une paire Ed25519 de graine
HKDF(link_secret, "link-sign") donne link_verify_pk, envoyée au serveur. Avec un mot de passe optionnel,
link_secret est remplacé par Argon2id(mot de passe, link_salt) ‖ link_secret passé dans HKDF ; link_salt et les
paramètres sont stockés dans le partage.
Accès : POST /v1/s/{share_id}/challenge (usage 4), signature du challenge avec la clé du lien,
POST /v1/s/{share_id}/verify, qui renvoie un jeton de scope share:<share_id> (expirant au plus tard avec le lien),
la clé chiffrée et le nom chiffré. Chaque ouverture incrémente le compteur ; au-delà de max_downloads, 403
LINK_EXHAUSTED. Le jeton ne donne accès qu'en lecture, à la ressource partagée et à sa sous-arborescence ; le partage
est revérifié à chaque requête, donc une révocation est immédiate. Seul le propriétaire peut créer un partage : un
destinataire ne peut pas re-partager.
9.3 Droits d'écriture
Avec write sur un dossier partagé, le destinataire peut y déposer des fichiers chiffrés avec la clé du dossier. Le
quota est imputé au propriétaire du dossier (upload_sessions.owner_id). Chaque index est signé par son auteur et le
client affiche qui a déposé quoi.
9.4 Révocation
DELETE /v1/shares/{id} coupe immédiatement l'accès côté serveur. La révocation n'est pas rétroactive. Pour un
dossier, le client peut faire une rotation : nouvelle clé de dossier, rechiffrement des clés et noms des enfants directs
et réémission des partages restants. Ce comportement doit figurer dans la documentation utilisateur.
10. Plans, quotas et marque
Un plan définit une limite en octets, une limite en nombre de fichiers et une durée. Le plan marqué is_default est
attribué à l'inscription. Chaque abonnement relie un utilisateur à un plan avec une date de début et une date
d'expiration (NULL = sans fin). users.brand indique la marque de rattachement.
Le quota consommé est la somme des fichiers validés (users.used_bytes, users.file_count, mis à jour dans les mêmes
transactions que les fichiers) et des réservations en cours (upload_sessions.reserved_bytes). À l'expiration d'un
abonnement, le compte passe en lecture seule : téléchargement autorisé, upload bloqué. Le sort des données après un
délai de grâce reste à décider.
11. Supervision des quotas Cloudflare
11.1 Sur chaque serveur
Un cron unique */3 * * * * :
- à chaque passage, interroge l'API GraphQL Analytics (
workersInvocationsAdaptive,sum { requests }, tout le compte, depuis 00:00 UTC) et écrit dans le KV la cléusage:{requests, limit, percent, updated_at}. Cela fait 480 écritures par jour, sous la limite gratuite de 1 000 ; - un passage sur trois (environ toutes les 9 minutes), lance les purges : challenges expirés, uploads orphelins, suppressions de dossiers.
Chaque réponse inclut X-Server-Usage: 42.7 (exposé par CORS). La valeur est mise en cache 60 s dans l'isolate.
GET /v1/status renvoie {server, usage} sans authentification. Avec le délai de 3 minutes, la valeur peut être
légèrement sous-estimée : le seuil de 80 % sert de marge.
11.2 Compte maître
Le Worker drive-master est dans son propre dépôt (014-Drive-Worker-Master) et sur son propre compte Cloudflare :
https://drive-master.master-014drive.workers.dev. Son registre est un Durable Object SQLite (disponible sur le plan
gratuit) : le maître ne dépend ni de la base partagée ni des serveurs qu'il surveille. Son cron (*/3 * * * *)
appelle GET /v1/status sur chaque serveur actif (45 au plus, sous la limite de sous-requêtes) et conserve 30 jours
d'historique.
| Requête | Authentification | Description |
|---|---|---|
GET / |
Aucune | Tableau de bord : état des serveurs, usage sur 24 h |
GET /api/servers/available |
Aucune | Serveurs actifs, sous 80 %, vus depuis moins de 10 minutes, triés par usage croissant (cache 30 s) |
GET /api/servers |
Aucune | Tous les serveurs et leur état |
GET /api/history?hours=24 |
Aucune | Historique d'usage |
POST /api/servers |
Jeton d'administration | {name, base_url} : enregistrement par la CI d'un serveur après son déploiement |
PATCH /api/servers/{name} |
Jeton d'administration | {enabled} : retirer un serveur de la liste proposée aux clients, ou l'y remettre |
DELETE /api/servers/{name} |
Jeton d'administration | Supprimer un serveur du registre |
L'usage des serveurs n'a rien de confidentiel : la lecture est publique. Les écritures exigent l'ADMIN_TOKEN des
serveurs, dont le maître ne connaît que l'empreinte SHA-256 (publiable, le jeton faisant 256 bits aléatoires).
Le client appelle /api/servers/available au démarrage, choisit un serveur, et bascule sur le suivant en cas d'erreur
ou si X-Server-Usage dépasse 80 %. Les jetons étant valables partout, la bascule est transparente. Attention : le
message de connexion contient api_origin, donc un challenge doit être signé pour le serveur auquel il sera soumis.
12. Schéma de base de données
Le DDL MySQL de référence est migrations/0001_init.sql dans le dépôt du serveur. Différences avec la version initiale :
- identifiants
CHAR(36)ASCII (MySQL refuseTEXTen clé primaire), binaires enBINARY/VARBINARY, compteurs enBIGINT UNSIGNED; plans.is_default;users.user_handle,users.password_login_enabled,users.recovery_bundle_enc;- nouvelle table
passkeys; upload_sessions.owner_id(quota imputé) etcreated_at;shares.link_saltetshares.link_kdf_params(liens protégés par mot de passe) ;- nouvelles tables
deletion_jobsettg_trash; - pas de tables
serversniusage_history: le registre est tenu par le maître (11.2).
13. Référence de l'API
Toutes les réponses incluent X-Server-Usage. Les erreurs ont la forme
{"error": {"code": "QUOTA_EXCEEDED", "message": "…"}}. La référence détaillée est openapi/openapi.yaml.
| Méthode et chemin | Authentification | Rôle |
|---|---|---|
POST /v1/auth/register |
Aucune | Inscription |
POST /v1/auth/prelogin |
Aucune | Sel, paramètres KDF, bundle |
POST /v1/auth/challenge |
Aucune | Challenge |
POST /v1/auth/verify |
Aucune | Jeton de session |
POST /v1/auth/password |
Session | Changement de mot de passe |
POST /v1/auth/passkeys/options |
Session | Options de création de passkey |
POST /v1/auth/passkeys |
Session | Enregistrer une passkey |
GET /v1/auth/passkeys |
Session | Lister ses passkeys |
DELETE /v1/auth/passkeys/{id} |
Session | Supprimer une passkey |
POST /v1/auth/passkey/challenge |
Aucune | Challenge de connexion par passkey |
POST /v1/auth/passkey/verify |
Aucune | Connexion par passkey |
PATCH /v1/auth/settings |
Session | Activer ou désactiver la connexion par mot de passe |
PUT /v1/auth/recovery |
Session | Enregistrer la copie de récupération du bundle |
POST /v1/auth/recovery/prelogin |
Aucune | Copie de récupération (ou valeur factice) |
GET /v1/me |
Session | Profil, plan, quotas consommés |
GET /v1/users/{username}/keys |
Session | Clés publiques d'un utilisateur |
POST /v1/uploads |
Session | Ouvrir une session d'upload |
PUT /v1/uploads/{id}/chunks/{i} |
Session | Envoyer un morceau |
POST /v1/uploads/{id}/commit |
Session | Envoyer l'index |
DELETE /v1/uploads/{id} |
Session | Annuler |
GET /v1/files/{id} |
Session ou partage | Index client et clé wrappée |
GET /v1/files/{id}/chunks/{i} |
Session ou partage | Télécharger un morceau |
PATCH /v1/files/{id} |
Session | Renommer un fichier |
POST /v1/files/{id}/move |
Session | Déplacer un fichier |
DELETE /v1/files/{id} |
Session | Supprimer un fichier |
POST /v1/folders |
Session | Créer un dossier |
GET /v1/folders/{id} |
Session ou partage | Lister un dossier |
PATCH /v1/folders/{id} |
Session | Renommer un dossier |
POST /v1/folders/{id}/move |
Session | Déplacer un dossier |
DELETE /v1/folders/{id} |
Session | Supprimer un dossier |
POST /v1/shares |
Session | Créer un partage |
GET /v1/shares/incoming |
Session | Partages reçus |
GET /v1/shares/outgoing |
Session | Partages émis |
DELETE /v1/shares/{id} |
Session | Révoquer |
POST /v1/s/{share_id}/challenge |
Aucune | Challenge d'un lien public |
POST /v1/s/{share_id}/verify |
Aucune | Jeton limité au partage |
POST /v1/reports |
Aucune | Signaler un lien |
GET /v1/status |
Aucune | Usage du serveur |
/v1/admin/* |
ADMIN_TOKEN |
migrate, bots, channels, channels/sync, usage/refresh, queues |
14. Déploiement multi-comptes
Le déploiement se fait uniquement par GitHub Actions. servers.json à la racine du dépôt du serveur :
[
{ "name": "srv-01", "account_id": "…", "secret_ref": "CF_TOKEN_SRV_01", "analytics_secret_ref": "CF_ANALYTICS_TOKEN_SRV_01" }
]
Champ optionnel hyperdrive_name, si le compte a plusieurs configurations Hyperdrive.
Le workflow lance les tests, lit ce fichier, génère une matrice et, pour chaque compte :
- prépare le compte via l'API Cloudflare : crée le namespace KV
drive-usages'il manque, retrouve la configuration Hyperdrive et désactive son cache, crée le sous-domaine workers.dev s'il manque, génère la configuration wrangler ; - lance
wrangler deploy; - pousse les secrets (
wrangler secret bulk) : le secret GitHubSHARED_SECRETS(JSON des secrets partagés, jetons des bots etADMIN_TOKEN) complété du jeton Analytics du compte ; - sur le premier serveur uniquement : applique les migrations (
POST /v1/admin/migrate) et synchronise les canaux Telegram (POST /v1/admin/channels/sync).
Routes d'administration (/v1/admin/*, jeton ADMIN_TOKEN) : migrate, bots, channels, channels/sync. La
synchronisation enregistre les couples (bot, canal) où le bot est administrateur avec les droits de publication et de
suppression, découverts via getUpdates (ajouts de moins de 24 h) ou fournis explicitement.
Ajouter un serveur : créer le compte et sa configuration Hyperdrive, ajouter ses deux jetons dans les secrets GitHub et
une entrée dans servers.json, puis pousser. Après son déploiement, chaque serveur s'enregistre auprès du maître dont
l'URL figure dans master.json. Le maître et la documentation ont leurs propres dépôts et déploiements. Aucune donnée n'étant stockée sur un serveur, en retirer un ne perd rien.
15. Documentation
Le site (dépôt 014-Drive-DOCS, publié sur Cloudflare Pages) comprend :
- la référence de l'API, affichée avec Scalar à partir de
openapi/openapi.yaml; - cette spécification ;
- le guide de la bibliothèque cliente de référence (
src/lib/du dépôt 014-Drive-Worker-WEBCLIENT, utilisée par l'application web), qui implémente toute la cryptographie et l'accès à l'API, dans le navigateur comme dans Node.js ; - les vecteurs de test (
vectors.json) : sorties attendues de chaque dérivation et de chaque format pour des entrées fixes, indispensables pour qu'un client tiers soit compatible. Ils sont vérifiés par les tests de la bibliothèque.
Formats précisés par la bibliothèque de référence :
| Élément | Chiffrement | AAD |
|---|---|---|
| Bundle (mot de passe) | XChaCha20-Poly1305, KEK Argon2id | "TGC-bundle-v1" |
| Bundle (passkey) | clé HKDF(prf_out, "TGC-passkey-bundle-v1") |
user_id ‖ SHA-256(credential_id) ‖ 0x01 |
| Bundle (récupération) | clé HKDF(code, "recovery-bundle-v1") |
"TGC-recovery-v1" ‖ username |
| Clé de dossier ou de fichier | clé du parent | "TGC-wrap-v1" ‖ item_id |
| Nom | HKDF(clé du parent, "name-enc") |
item_id |
Métadonnées du fichier (JSON {name, mime, size, modified}) |
FEK | "TGC-meta-v1" ‖ file_id |
| Clé partagée avec un utilisateur, nom du partage | sealed box X25519 | — |
| Clé d'un lien public | HKDF(secret, "link-enc") |
"TGC-link-key-v1" ‖ resource_id |
| Nom d'un lien public | HKDF(secret, "link-enc") |
"TGC-link-name-v1" ‖ resource_id |
Avec un mot de passe de lien, le matériau d'entrée de HKDF est Argon2id(mot de passe, link_salt) ‖ secret.
L'empreinte affichée pour comparer des clés est SHA-256("TGC-fingerprint-v1" ‖ sign_pk ‖ box_pk), 10 premiers
octets en hexadécimal par groupes de 4.
16. Application web
L'application (https://app-014drive.pages.dev) s'exécute entièrement dans le navigateur ; Cloudflare Pages ne sert que des fichiers statiques. Elle utilise la bibliothèque de référence et ajoute :
- connexion par mot de passe (Argon2id dans le navigateur), par passkey (bouton ou autoremplissage du champ de nom d'utilisateur, médiation « conditional ») ou par code de récupération ;
- inscription avec estimation d'entropie zxcvbn (score 3 sur 4 minimum, 12 caractères au moins) ;
- explorateur chiffré : dossiers, envoi par bouton ou glisser-déposer avec progression, téléchargement morceau par morceau (écrit directement sur le disque avec l'API File System Access quand elle existe), renommage, déplacement, suppression ;
- partage avec un utilisateur après comparaison de l'empreinte de ses clés, mémorisée localement (confiance au premier usage, alerte si elle change) ; liens publics avec expiration, nombre d'ouvertures et mot de passe ;
- page publique
/s/{share_id}#{secret}pour ouvrir un lien sans compte ; - paramètres : passkeys, mode « passkey uniquement », code de récupération, changement de mot de passe.
Le bundle de clés n'est conservé qu'en mémoire : un rechargement demande de se reconnecter. Une politique de sécurité du
contenu stricte (public/_headers) n'autorise que les scripts du site et les appels aux Workers de 014-Drive, car une
application chiffrée de bout en bout ne vaut que si son code n'est pas altéré.
Annexe A. Changements par rapport à la version initiale
| Sujet | Changement |
|---|---|
| Hyperdrive | Une configuration par compte, cache désactivé ; DB_URL/DB_TOKEN supprimés |
| Secrets | CF_ANALYTICS_TOKEN propre à chaque compte ; ajout de CF_ACCOUNT_ID, SERVER_NAME, variables WebAuthn |
| Formats | Encodages, bundle (97/137 octets), kdf_params, clés wrappées (72 octets) et AAD précisés |
| Messages signés | Séparateurs \n pour éviter toute ambiguïté entre l'origine et le challenge |
| Challenges | Format binaire avec champ d'usage, pour qu'un challenge de connexion ne serve pas à un lien public |
| Jetons | Charge utile en clair (`sub |
| Cron | Un seul cron toutes les 3 minutes ; purges un passage sur trois |
| Purge | File tg_trash vidée par le cron, deleteMessages par lots de 100, verrouillage SKIP LOCKED entre serveurs |
| Upload | file_id généré par le serveur ; tailles des morceaux fixées à l'ouverture ; 1000 morceaux au plus ; nom en doublon → 409 sans annulation |
| Manifeste | Structure {v, index, replicas} |
| Telegram | Tirage aléatoire de deux canaux distincts ; limite d'environ 20 messages par minute et par canal |
| Schéma | DDL MySQL, tables passkeys et deletion_jobs, colonnes ajoutées (section 12) |
| Maître | Dépôt et compte dédiés ; registre en Durable Object SQLite ; lecture et tableau de bord publics ; écritures par jeton d'administration |
| API | Routes passkeys et PATCH /v1/auth/settings ajoutées à la table |