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épendance libsodium-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.

  1. Tirez au hasard sign_seed, box_sk, root_key (32 octets chacun) et un sel de 16 octets.
  2. Calculez les clés publiques : sign_pk (Ed25519, crypto_sign_seed_keypair(sign_seed)) et box_pk (X25519, crypto_scalarmult_base(box_sk)).
  3. 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), puis bundle_enc = seal(KEK, trousseau, "TGC-bundle-v1") (137 octets).
  4. Créez le dossier racine : un UUID et une clé aléatoire de 32 octets, enveloppée par root_key.
  5. Signez la preuve "REGISTER-v1" ‖ username ‖ sign_pk ‖ box_pk avec 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}

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
}

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.

  1. Vérifiez l'index. Retirez sig, sérialisez le reste en JSON canonique (RFC 8785 : clés triées récursivement, sans espaces), vérifiez sig (Ed25519) avec author_pk. Pour savoir qui est l'auteur, comparez author_pk à vos propres clés ou à celles de author_username (GET /v1/users/{username}/keys). Un fichier déposé par un lien éditeur (author_link: true) est signé par la clé du lien.
  2. Clé du fichier (FEK) : unwrap(clé du dossier folder_id, file_id, wrapped_fek).
  3. Informations : open(FEK, index.meta_enc, "TGC-meta-v1" ‖ file_id) donne le JSON {"name", "mime", "size", "modified"} (taille réelle, date en secondes).
  4. Morceaux : pour le morceau i, demandez GET /v1/files/c/{ref} avec la première référence de chunk_refs[i], de la forme 1234567/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)
  1. Déchiffrez chaque morceau avec la FEK et cet AAD, et mettez-les bout à bout. La taille totale doit être égale à size des 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.

  1. 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.

  1. 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.

  1. 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": "…"}

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.