Créer un client 014-Drive
Ce guide donne tout le nécessaire pour écrire votre propre client : trouver un serveur, se connecter, parcourir ses dossiers, récupérer et envoyer des fichiers, ouvrir un lien public. Il ne dit rien de l'interface : c'est à vous de la concevoir. Toute la cryptographie se fait dans votre client ; le serveur ne fait que stocker et contrôler l'accès.
Exemple complet.
client-minimal.mjs(Node.js 20, environ 230 lignes, seule dépendancelibsodium-wrappers-sumo) suit ce guide pas à pas : inscription, connexion, liste, envoi, récupération, lien public. Il est testé contre le service réel, et ses fichiers s'ouvrent dans l'application web.
1. Bases
Bibliothèques
libsodium couvre tout (des liaisons existent pour la plupart des langages) :
Argon2id, XChaCha20-Poly1305 IETF, Ed25519, X25519 (crypto_box_seal). Il faut aussi SHA-256, HMAC-SHA256 et
HKDF-SHA256, disponibles partout (WebCrypto, OpenSSL…).
Conventions
| Élément | Format |
|---|---|
| Champs binaires dans le JSON | base64 standard avec padding |
| Jetons, challenges, secret d'un lien | base64url sans padding |
| Identifiants | UUID v4 en minuscules |
| Dates | secondes Unix |
| Noms d'utilisateur | ^[a-z0-9][a-z0-9_.-]{2,31}$, toujours envoyés en minuscules |
| Erreurs | {"error": {"code": "QUOTA_EXCEEDED", "message": "…"}} |
‖ |
concaténation d'octets ; les chaînes sont en UTF-8 |
Trois opérations reviennent partout
Chiffrer (seal) : XChaCha20-Poly1305 IETF avec un nonce aléatoire de 24 octets. Résultat :
nonce (24) ‖ texte chiffré ‖ tag (16). Déchiffrer (open) : l'inverse ; un échec du tag signifie une clé ou
des données incorrectes. Chaque usage a son propre AAD (données associées), indiqué à chaque étape.
Dériver (HKDF(clé, info)) : HKDF-SHA256, sel vide, info en ASCII, 32 octets de sortie.
Envelopper une clé (wrap(parent, id, clé)) : seal(parent, clé, "TGC-wrap-v1" ‖ id), où id est l'UUID
(texte) de l'élément protégé. 72 octets. unwrap est l'open correspondant.
seal(clé, clair, aad) = nonce(24) ‖ XChaCha20-Poly1305(clé, nonce, clair, aad)
HKDF(ikm, info) = HKDF-SHA256(sel vide, ikm, info ASCII) → 32 octets
wrap(parent, id, clé) = seal(parent, clé, "TGC-wrap-v1" ‖ id)
2. Trouver un serveur
Le service a plusieurs serveurs d'API équivalents. Demandez la liste à jour, du moins chargé au plus chargé :
GET https://drive-master.master-014drive.workers.dev/api/servers/available
{ "servers": [ { "name": "srv-01", "base_url": "https://drive-api.srv01-014drive.workers.dev", "percent": 1.3 } ] }
Utilisez le premier. Passez au suivant si une requête échoue (réseau, 5xx) ou si l'en-tête X-Server-Usage d'une
réponse dépasse 80. Les jetons sont valables sur tous les serveurs, avec une exception : un challenge doit être signé
pour le serveur auquel vous le renverrez (étape 4). Gardez la liste en cache pour le cas où l'annuaire ne répond pas.
Les codes STORAGE_BUSY et STORAGE_UNAVAILABLE (503) et 429 sont temporaires : réessayez après Retry-After.
3. Créer un compte
Facultatif : vous pouvez aussi utiliser un compte créé dans l'application.
- Tirez au hasard
sign_seed,box_sk,root_key(32 octets chacun) et un sel de 16 octets. - Calculez les clés publiques :
sign_pk(Ed25519,crypto_sign_seed_keypair(sign_seed)) etbox_pk(X25519,crypto_scalarmult_base(box_sk)). - Le trousseau fait 97 octets :
0x01 ‖ sign_seed ‖ box_sk ‖ root_key. Chiffrez-le avec la clé dérivée du mot de passe :KEK = Argon2id(mot de passe, sel)avec{"alg":"argon2id","v":19,"m":65536,"t":3,"p":1}(m en Kio, sortie 32 octets), puisbundle_enc = seal(KEK, trousseau, "TGC-bundle-v1")(137 octets). - Créez le dossier racine : un UUID et une clé aléatoire de 32 octets, enveloppée par
root_key. - Signez la preuve
"REGISTER-v1" ‖ username ‖ sign_pk ‖ box_pkavec Ed25519.
POST /v1/auth/register
{
"username": "alice",
"salt": "…", "kdf_params": {"alg":"argon2id","v":19,"m":65536,"t":3,"p":1},
"bundle_enc": "…", "sign_pk": "…", "box_pk": "…",
"root_folder": { "folder_id": "uuid", "wrapped_key": "wrap(root_key, folder_id, clé du dossier)" },
"proof": "…"
}
Réponse 201 {user_id, root_folder_id}. Erreur USERNAME_TAKEN si le nom est pris. Imposez un mot de passe long
(12 caractères au moins) : le trousseau chiffré est accessible avant authentification.
4. Se connecter
POST /v1/auth/prelogin {"username": "alice"} → {salt, kdf_params, bundle_enc}
KEK = Argon2id(mot de passe, salt, kdf_params)
trousseau = open(KEK, bundle_enc, "TGC-bundle-v1") → 0x01 ‖ sign_seed ‖ box_sk ‖ root_key
POST /v1/auth/challenge {"username": "alice"} → {challenge, expires_at}
signature = Ed25519(sign_sk, "AUTH-v1\n" + origine + "\n" + challenge)
POST /v1/auth/verify {"challenge", "signature"} → {token, expires_at, user_id, root_folder_id}
origineest l'origine du serveur appelé, sans barre finale (https://drive-api.srv01-014drive.workers.dev). Envoyez le challenge et la vérification au même serveur.- Un mauvais mot de passe et un compte inexistant sont indiscernables :
openéchoue dans les deux cas (le serveur renvoie un faux trousseau pour un nom inconnu). - Le jeton se passe en
Authorization: Bearer <token>et dure 15 minutes. Renouvelez-le une minute avant l'échéance, ou sur un401, en refaisant challenge et vérification : le trousseau est déjà en mémoire, inutile de redemander le mot de passe. - Ne gardez pas le trousseau sur disque en clair.
Code de récupération (32 caractères hexadécimaux, soit 16 octets) : POST /v1/auth/recovery/prelogin {"username"} renvoie recovery_bundle_enc ; la clé est HKDF(code, "recovery-bundle-v1") et l'AAD
"TGC-recovery-v1" ‖ username. La suite est identique. Les passkeys sont liées au domaine de l'application web et ne
concernent pas un client tiers.
5. Parcourir ses dossiers
GET /v1/folders/{id}
{
"folder_id": "…", "wrapped_key": "…", "permission": "owner",
"folders": [ { "folder_id": "…", "wrapped_key": "…", "name_enc": "…", "created_at": 1767225600 } ],
"files": [ { "file_id": "…", "wrapped_fek": "…", "name_enc": "…", "size": 1090, "chunk_count": 1, "created_at": 1767225600 } ],
"next": null
}
- Clé du dossier racine :
unwrap(root_key, root_folder_id, wrapped_key). Pour un sous-dossier ou un fichier :unwrap(clé du dossier parent, id de l'enfant, wrapped_key ou wrapped_fek). - Noms : chaque dossier
KdonneK_nom = HKDF(K, "name-enc"); le nom d'un enfant estopen(K_nom, name_enc, id de l'enfant)(l'AAD est l'UUID seul). sizeest la taille chiffrée : taille réelle =size − 45 × chunk_count.- S'il y a plus de 1000 éléments,
nextvaut{"after_d": "…", "after_f": "…"}: redemandez le dossier avec ces deux paramètres de requête.
6. Récupérer un fichier
GET /v1/files/{id}
La réponse contient folder_id, wrapped_fek, name_enc, l'index signé par l'auteur et chunk_refs.
- Vérifiez l'index. Retirez
sig, sérialisez le reste en JSON canonique (RFC 8785 : clés triées récursivement, sans espaces), vérifiezsig(Ed25519) avecauthor_pk. Pour savoir qui est l'auteur, comparezauthor_pkà vos propres clés ou à celles deauthor_username(GET /v1/users/{username}/keys). Un fichier déposé par un lien éditeur (author_link: true) est signé par la clé du lien. - Clé du fichier (FEK) :
unwrap(clé du dossier folder_id, file_id, wrapped_fek). - Informations :
open(FEK, index.meta_enc, "TGC-meta-v1" ‖ file_id)donne le JSON{"name", "mime", "size", "modified"}(taille réelle, date en secondes). - Morceaux : pour le morceau
i, demandezGET /v1/files/c/{ref}avec la première référence dechunk_refs[i], de la forme1234567/AbCd…(les suivantes servent de secours). Cette adresse ne prend ni jeton ni en-tête. Chaque morceau a ce format :
octets 0-3 "TGC1"
octet 4 0x01 (version)
octets 5-28 nonce (24)
octets 29- texte chiffré ‖ tag (16)
AAD = file_id (16 octets binaires de l'UUID) ‖ i (uint32 big-endian)
- Déchiffrez chaque morceau avec la FEK et cet AAD, et mettez-les bout à bout. La taille totale doit être égale à
sizedes informations.
Vous pouvez télécharger plusieurs morceaux à la fois (8 dans l'application) et les écrire dans l'ordre. Si
chunk_refs est absent, demandez GET /v1/files/chunk avec votre jeton et les en-têtes X-File-Id et
X-Chunk-Index.
7. Envoyer un fichier
Chaque morceau contient au plus 19 922 899 octets en clair (19 922 944 une fois chiffré, avec ses 45 octets
d'en-tête et de tag). Pour un contenu de n octets : chunk_count = max(1, ⌈n / 19 922 899⌉) (10 000 au plus),
total_size = n + 45 × chunk_count.
- Ouvrir une session :
POST /v1/uploads {"folder_id": "…", "chunk_count": 1, "total_size": 1045}
→ 201 {"upload_id", "file_id", "upload_token", "expires_at"}
Le serveur réserve la place et fixe file_id. Tirez une FEK aléatoire de 32 octets.
- Envoyer chaque morceau (format de l'étape 6, AAD
file_id ‖ i) :
PUT /v1/uploads/chunk
Authorization: Bearer <jeton de session>
X-Upload-Token: <upload_token>
X-Chunk-Index: 0
Content-Type: application/octet-stream
<morceau chiffré>
Tous les morceaux sauf le dernier font exactement 19 922 944 octets. Après une coupure, GET /v1/uploads/{upload_id}
donne la liste received : n'envoyez que les morceaux manquants, avec la même FEK. Une session vit 24 heures.
- Valider avec l'index signé :
{
"v": 1, "file_id": "…", "upload_id": "…", "chunk_count": 1,
"chunks": [ { "i": 0, "size": 1090 } ],
"total_size": 1090,
"meta_enc": "seal(FEK, {\"name\",\"mime\",\"size\",\"modified\"}, \"TGC-meta-v1\" ‖ file_id)",
"author_pk": "votre sign_pk",
"sig": "Ed25519 du JSON canonique de tout ce qui précède"
}
POST /v1/uploads/{upload_id}/commit
{"index": {…}, "wrapped_fek": "wrap(clé du dossier, file_id, FEK)", "name_enc": "…", "name_hmac": "…"}
name_enc = seal(K_nom, nom, file_id)avecK_nom = HKDF(clé du dossier, "name-enc").name_hmac = HMAC-SHA256(HKDF(clé du dossier, "name-hmac"), minuscules(NFC(nom))): le serveur refuse un nom déjà pris dans le dossier (409 NAME_EXISTS, la session reste ouverte : réessayez avec un autre nom).- Pour remplacer un fichier par une nouvelle version, ajoutez
"replaces": "<file_id de l'ancien>".
Après une erreur définitive (quota, droits, 422 INVALID_INDEX), annulez avec DELETE /v1/uploads/{upload_id} pour
libérer la réservation.
8. Organiser
| Action | Requête | Corps |
|---|---|---|
| Créer un dossier | POST /v1/folders |
{parent_id, folder_id, wrapped_key, name_enc, name_hmac} : UUID et clé tirés par vous |
| Renommer | PATCH /v1/folders/{id} ou /v1/files/{id} |
{name_enc, name_hmac} |
| Déplacer un dossier | POST /v1/folders/{id}/move |
{new_parent_id, wrapped_key, name_enc, name_hmac} |
| Déplacer un fichier | POST /v1/files/{id}/move |
{new_folder_id, wrapped_fek, name_enc, name_hmac} |
| Supprimer | DELETE /v1/folders/{id} ou /v1/files/{id} |
— |
| Supprimer plusieurs enfants | POST /v1/folders/{id}/delete |
{files: [ids], folders: [ids]} (1000 au plus) |
Pour un déplacement, la clé de l'élément est ré-enveloppée avec la clé du nouveau parent, et le nom rechiffré avec ses sous-clés. Le dossier racine ne peut être ni renommé, ni déplacé, ni supprimé.
9. Partages
Reçus. GET /v1/shares/incoming liste ce qu'on vous a partagé. wrapped_key et name_enc y sont des
sealed boxes pour votre clé d'échange : crypto_box_seal_open(wrapped_key, box_pk, box_sk) donne la clé du
dossier ou du fichier (resource_id), à utiliser comme aux étapes 5 et 6. permissions vaut read ou write.
Émis. GET /v1/users/{username}/keys donne sign_pk et box_pk du destinataire ; montrez-en l'empreinte
(SHA-256("TGC-fingerprint-v1" ‖ sign_pk ‖ box_pk), 10 premiers octets en hexadécimal) pour qu'elle soit comparée
de vive voix. Puis POST /v1/shares {resource_type, resource_id, recipient_username, wrapped_key, name_enc, permissions}, avec la clé et le nom scellés par crypto_box_seal pour box_pk.
10. Ouvrir un lien public
Adresse d'un lien : https://drive.014.fr/s/{share_id}#{secret}, où secret fait 32 octets en base64url.
POST /v1/s/{share_id}/challenge → {challenge, link_salt, link_kdf_params}
ikm = secret (lien sans mot de passe)
= Argon2id(mot de passe, link_salt, link_kdf_params) ‖ secret (si link_salt n'est pas null)
K_lien = HKDF(ikm, "link-enc")
clé de signature Ed25519 = crypto_sign_seed_keypair(HKDF(ikm, "link-sign"))
signature = Ed25519("LINK-v1\n" + origine + "\n" + challenge)
POST /v1/s/{share_id}/verify {"challenge", "signature"}
→ {token, expires_at, resource_type, resource_id, wrapped_key, name_enc, permissions}
clé de la ressource = open(K_lien, wrapped_key, "TGC-link-key-v1" ‖ resource_id)
nom = open(K_lien, name_enc, "TGC-link-name-v1" ‖ resource_id)
Le token obtenu s'utilise comme un jeton de session, limité à la ressource partagée et à ses sous-dossiers :
GET /v1/files/{resource_id} (la FEK est la clé de la ressource) ou GET /v1/folders/{resource_id} puis la suite comme
à l'étape 5. Un lien éditeur (permissions: "write") permet aussi d'envoyer, créer, renommer, déplacer et supprimer
dans le dossier ; les index que vous y déposez sont signés avec la clé de signature du lien.
Erreurs : 401 AUTH_FAILED (mauvais mot de passe ou lien incomplet), 404 LINK_NOT_FOUND (inexistant, expiré ou
désactivé), 403 LINK_EXHAUSTED (nombre d'ouvertures atteint). Chaque ouverture compte : pour renouveler le jeton,
refaites challenge et vérification.
11. Vérifier son client
Les vecteurs de test donnent, pour des entrées fixes, la sortie attendue de chaque étape : Argon2id, trousseau, preuve d'inscription, message de connexion, enveloppe de clé, sous-clés de dossier, nom, morceau, informations du fichier, index signé, lien, HKDF et empreinte. Si votre client les reproduit, il est compatible.
12. Routes utilisées
| Requête | Authentification | Étape |
|---|---|---|
GET …/api/servers/available (annuaire) |
— | 2 |
POST /v1/auth/register |
— | 3 |
POST /v1/auth/prelogin, /challenge, /verify |
— | 4 |
POST /v1/auth/recovery/prelogin |
— | 4 |
GET /v1/me |
session | profil, forfait, espace utilisé |
GET /v1/folders/{id} |
session ou lien | 5 |
GET /v1/files/{id} |
session ou lien | 6 |
GET /v1/files/c/{ref} |
— | 6 |
POST /v1/uploads, PUT /v1/uploads/chunk, POST /v1/uploads/{id}/commit |
session ou lien éditeur | 7 |
GET / DELETE /v1/uploads/{id} |
session ou lien éditeur | 7 |
POST /v1/folders, PATCH, …/move, DELETE |
session ou lien éditeur | 8 |
GET /v1/shares/incoming, GET /v1/users/{u}/keys, POST /v1/shares |
session | 9 |
POST /v1/s/{id}/challenge, /verify |
— | 10 |
La référence de l'API décrit chaque route.