[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"BLOG_POST_fr_receiving-an-encrypted-transfer":3,"BLOG_SURROUND_fr_receiving-an-encrypted-transfer":1399},{"id":4,"title":5,"authors":6,"body":9,"category":1379,"date":1380,"description":1381,"extension":1382,"image":1383,"meta":1384,"navigation":90,"path":1385,"seo":1386,"sitemap":1390,"stem":1391,"tags":1392,"__hash__":1398},"blog\u002Fblog\u002Ffr\u002Freceiving-an-encrypted-transfer.md","Recevoir un transfert chiffré : du lien au fichier sur votre disque",[7],{"name":8},"Emilien Mantel",{"type":10,"value":11,"toc":1370},"minimark",[12,16,19,24,27,30,121,124,128,137,140,143,268,271,274,278,281,368,375,379,382,784,787,790,794,797,806,909,912,919,1064,1080,1083,1093,1096,1099,1176,1179,1182,1185,1189,1195,1337,1340,1343,1347,1355,1366],[13,14,15],"p",{},"Vous recevez un e-mail : un collègue vous a envoyé un dossier. Vous cliquez sur le lien, vous saisissez la passphrase\nqu'il vous a transmise par téléphone, et la liste des fichiers apparaît. Un clic sur « Tout télécharger », et quelques\nminutes plus tard les fichiers sont sur votre disque.",[13,17,18],{},"Vu de l'extérieur, c'est un téléchargement comme un autre. Mais le serveur qui vous a servi ces fichiers ne sait pas\nles lire : il ne détient ni leur contenu en clair, ni leurs noms, ni la moindre clé. Tout ce qui les rend lisibles\ns'est passé dans votre navigateur. Cet article suit ce trajet dans l'ordre, du lien jusqu'au fichier enregistré. Les\nextraits viennent du front de Retyc (Nuxt, TypeScript) et de l'API (FastAPI). Ils sont raccourcis pour la lecture, et\nchacun indique le fichier d'origine.",[20,21,23],"h2",{"id":22},"le-lien-ne-suffit-pas-toujours","Le lien ne suffit pas toujours",[13,25,26],{},"Un lien de transfert finit par circuler : transféré à un collègue, collé dans un ticket, affiché en aperçu par une\nmessagerie. Pour les données chiffrées, ce n'est pas grave, puisque personne ne peut les lire sans la clé. Mais\ndistribuer le contenu chiffré à quiconque tient le lien reste inutile, et nous ne le faisons pas quand ce n'est pas\nnécessaire.",[13,28,29],{},"Tout dépend de la façon dont l'expéditeur a protégé son transfert. Avec une passphrase, le lien est ouvert : le\ndestinataire n'a pas besoin de compte, c'est la passphrase qui protège les fichiers. Sans passphrase, le transfert n'a\nété chiffré que pour des comptes Retyc précis, et l'API le réserve à ces comptes :",[31,32,38],"pre",{"className":33,"code":34,"filename":35,"language":36,"meta":37,"style":37},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","def assert_share_access(share: ShareModel, current_user: Optional[CurrentUserModel]) -> None:\n    \"\"\"A share without passphrase can only be decrypted by its recipients: restrict its\n    metadata and its chunks to the sender and the recipients, so that holding the link\n    is not enough to harvest the encrypted payload.\n    \"\"\"\n    if share.use_passphrase:\n        return\n\n    if current_user is None:\n        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Authentication required\")\n\n    if current_user.id != share.owner_id and current_user.id not in share.recipients_ids:\n        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"You are not a recipient of this share\")\n","app\u002Fapi\u002Fprivate\u002Fshare\u002Fdependencies.py","python","",[39,40,41,49,55,61,67,73,79,85,92,98,104,109,115],"code",{"__ignoreMap":37},[42,43,46],"span",{"class":44,"line":45},"line",1,[42,47,48],{},"def assert_share_access(share: ShareModel, current_user: Optional[CurrentUserModel]) -> None:\n",[42,50,52],{"class":44,"line":51},2,[42,53,54],{},"    \"\"\"A share without passphrase can only be decrypted by its recipients: restrict its\n",[42,56,58],{"class":44,"line":57},3,[42,59,60],{},"    metadata and its chunks to the sender and the recipients, so that holding the link\n",[42,62,64],{"class":44,"line":63},4,[42,65,66],{},"    is not enough to harvest the encrypted payload.\n",[42,68,70],{"class":44,"line":69},5,[42,71,72],{},"    \"\"\"\n",[42,74,76],{"class":44,"line":75},6,[42,77,78],{},"    if share.use_passphrase:\n",[42,80,82],{"class":44,"line":81},7,[42,83,84],{},"        return\n",[42,86,88],{"class":44,"line":87},8,[42,89,91],{"emptyLinePlaceholder":90},true,"\n",[42,93,95],{"class":44,"line":94},9,[42,96,97],{},"    if current_user is None:\n",[42,99,101],{"class":44,"line":100},10,[42,102,103],{},"        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Authentication required\")\n",[42,105,107],{"class":44,"line":106},11,[42,108,91],{"emptyLinePlaceholder":90},[42,110,112],{"class":44,"line":111},12,[42,113,114],{},"    if current_user.id != share.owner_id and current_user.id not in share.recipients_ids:\n",[42,116,118],{"class":44,"line":117},13,[42,119,120],{},"        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"You are not a recipient of this share\")\n",[13,122,123],{},"Un visiteur non connecté est invité à se connecter, un compte qui n'est pas destinataire reçoit un refus. Dans les deux\ncas, aucun octet chiffré ne sort de nos serveurs. Un transfert expiré ou désactivé par son expéditeur ne répond plus du\ntout, quel que soit le compte.",[20,125,127],{"id":126},"déverrouiller-le-transfert","Déverrouiller le transfert",[13,129,130,131,136],{},"Une fois l'accès accordé, le navigateur reçoit la description du transfert : son titre, et plusieurs « enveloppes »\nchiffrées contenant la clé de session, celle qui a servi à chiffrer les fichiers. Nous avons décrit comment ces\nenveloppes sont fabriquées dans ",[132,133,135],"a",{"href":134},"\u002Ffr\u002Fblog\u002Fanatomy-of-an-encrypted-upload","l'anatomie d'un upload chiffré",". Côté\ndestinataire, il s'agit de les ouvrir.",[13,138,139],{},"Si vous avez un compte et que le transfert vous est destiné, votre clé privée, déverrouillée dans le navigateur à la\nconnexion, ouvre directement la sienne. Vous n'avez rien à saisir.",[13,141,142],{},"Sans compte, c'est la passphrase qui sert, en deux temps :",[31,144,149],{"className":145,"code":146,"filename":147,"language":148,"meta":37,"style":37},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F Étape 1 : la passphrase ouvre une clé privée éphémère (scrypt)\nconst ephemeralPrivateKey = await decryptStringWithPassphrase(\n    shareInfo.value.ephemeral_private_key_enc,\n    data.password\n)\n\n\u002F\u002F Étape 2 : cette clé éphémère ouvre la clé de session du transfert\nsessionPrivateKey.value = await decryptStringWithIdentity(\n    shareInfo.value.session_private_key_enc_for_passphrase,\n    ephemeralPrivateKey\n)\n","app\u002Fpages\u002Ftransfer\u002F[slug].vue","ts",[39,150,151,157,182,201,211,216,220,225,244,259,264],{"__ignoreMap":37},[42,152,153],{"class":44,"line":45},[42,154,156],{"class":155},"sHwdD","\u002F\u002F Étape 1 : la passphrase ouvre une clé privée éphémère (scrypt)\n",[42,158,159,163,167,171,175,179],{"class":44,"line":51},[42,160,162],{"class":161},"spNyl","const",[42,164,166],{"class":165},"sTEyZ"," ephemeralPrivateKey ",[42,168,170],{"class":169},"sMK4o","=",[42,172,174],{"class":173},"s7zQu"," await",[42,176,178],{"class":177},"s2Zo4"," decryptStringWithPassphrase",[42,180,181],{"class":165},"(\n",[42,183,184,187,190,193,195,198],{"class":44,"line":57},[42,185,186],{"class":165},"    shareInfo",[42,188,189],{"class":169},".",[42,191,192],{"class":165},"value",[42,194,189],{"class":169},[42,196,197],{"class":165},"ephemeral_private_key_enc",[42,199,200],{"class":169},",\n",[42,202,203,206,208],{"class":44,"line":63},[42,204,205],{"class":165},"    data",[42,207,189],{"class":169},[42,209,210],{"class":165},"password\n",[42,212,213],{"class":44,"line":69},[42,214,215],{"class":165},")\n",[42,217,218],{"class":44,"line":75},[42,219,91],{"emptyLinePlaceholder":90},[42,221,222],{"class":44,"line":81},[42,223,224],{"class":155},"\u002F\u002F Étape 2 : cette clé éphémère ouvre la clé de session du transfert\n",[42,226,227,230,232,235,237,239,242],{"class":44,"line":87},[42,228,229],{"class":165},"sessionPrivateKey",[42,231,189],{"class":169},[42,233,234],{"class":165},"value ",[42,236,170],{"class":169},[42,238,174],{"class":173},[42,240,241],{"class":177}," decryptStringWithIdentity",[42,243,181],{"class":165},[42,245,246,248,250,252,254,257],{"class":44,"line":94},[42,247,186],{"class":165},[42,249,189],{"class":169},[42,251,192],{"class":165},[42,253,189],{"class":169},[42,255,256],{"class":165},"session_private_key_enc_for_passphrase",[42,258,200],{"class":169},[42,260,261],{"class":44,"line":100},[42,262,263],{"class":165},"    ephemeralPrivateKey\n",[42,265,266],{"class":44,"line":106},[42,267,215],{"class":165},[13,269,270],{},"La première étape prend volontairement un peu de temps : scrypt est conçu pour rendre chaque essai coûteux, et donc\nles attaques par force brute très lentes. Une passphrase erronée ne produit pas un résultat faux mais un échec net, et\nla page vous le dit. Dans tous les cas, la passphrase n'est jamais envoyée au serveur : elle est vérifiée par le seul\nfait qu'elle ouvre, ou non, l'enveloppe.",[13,272,273],{},"La clé de session obtenue ne quitte pas la page. Elle sert d'abord à déchiffrer le message éventuel de l'expéditeur,\npuis la liste des fichiers.",[20,275,277],{"id":276},"la-liste-des-fichiers","La liste des fichiers",[13,279,280],{},"Le serveur stocke pour chaque fichier un nom chiffré, une taille et un nombre de morceaux. Le navigateur déchiffre les\nnoms un à un pour afficher la liste :",[31,282,285],{"className":145,"code":283,"filename":284,"language":148,"meta":37,"style":37},"for await (const item of fullScanFiles(props.shareId)) {\n    const name_dec = await decryptStringWithIdentity(item.name_enc, props.sessionPrivateKey)\n    \u002F\u002F ...\n}\n","app\u002Fcomponents\u002Ftransfer\u002Fmodal\u002FDownloadAll.vue",[39,286,287,319,358,363],{"__ignoreMap":37},[42,288,289,292,294,297,299,302,305,308,311,313,316],{"class":44,"line":45},[42,290,291],{"class":173},"for",[42,293,174],{"class":173},[42,295,296],{"class":165}," (",[42,298,162],{"class":161},[42,300,301],{"class":165}," item ",[42,303,304],{"class":169},"of",[42,306,307],{"class":177}," fullScanFiles",[42,309,310],{"class":165},"(props",[42,312,189],{"class":169},[42,314,315],{"class":165},"shareId)) ",[42,317,318],{"class":169},"{\n",[42,320,321,324,327,330,332,334,338,341,343,346,349,352,354,356],{"class":44,"line":51},[42,322,323],{"class":161},"    const",[42,325,326],{"class":165}," name_dec",[42,328,329],{"class":169}," =",[42,331,174],{"class":173},[42,333,241],{"class":177},[42,335,337],{"class":336},"swJcz","(",[42,339,340],{"class":165},"item",[42,342,189],{"class":169},[42,344,345],{"class":165},"name_enc",[42,347,348],{"class":169},",",[42,350,351],{"class":165}," props",[42,353,189],{"class":169},[42,355,229],{"class":165},[42,357,215],{"class":336},[42,359,360],{"class":44,"line":57},[42,361,362],{"class":155},"    \u002F\u002F ...\n",[42,364,365],{"class":44,"line":63},[42,366,367],{"class":169},"}\n",[13,369,370,371,374],{},"Pour un dossier envoyé tel quel, le nom déchiffré contient son chemin complet, par exemple ",[39,372,373],{},"contrats\u002F2026\u002Favenant.pdf",".\nC'est ce qui permettra plus loin de reconstituer l'arborescence d'origine.",[20,376,378],{"id":377},"récupérer-et-déchiffrer-le-contenu","Récupérer et déchiffrer le contenu",[13,380,381],{},"Chaque fichier a été découpé en morceaux à l'envoi, chacun chiffré séparément. Le navigateur les demande à l'API,\nquatre à la fois par défaut, et les déchiffre dans un Web Worker au fur et à mesure qu'ils arrivent. Deux limites\nencadrent ce travail : le nombre de téléchargements simultanés, et le nombre de morceaux gardés en mémoire en\nattendant leur tour d'écriture :",[31,383,386],{"className":145,"code":384,"filename":385,"language":148,"meta":37,"style":37},"const LOOKAHEAD = 2 * BATCH_SIZE \u002F\u002F morceaux gardés à la fois, écriture comprise\n\nfunction startNextChunks(): void {\n    while (!failed && next \u003C chunkCount && inFlight \u003C BATCH_SIZE && held.size \u003C LOOKAHEAD) {\n        const chunk_id = next++\n        inFlight++\n        const chunk = doDownload(file_transfer, chunk_id)\n            .then(res => decryptWithIdentity(res, session_private_key))\n        \u002F\u002F Un créneau libéré relance aussitôt un téléchargement\n        chunk.then(() => {\n            inFlight--;\n            startNextChunks()\n        }, () => {\n            inFlight--;\n            failed = true\n        })\n        held.set(chunk_id, chunk)\n    }\n}\n\nstartNextChunks()\nfor (let chunk_id = 0; chunk_id \u003C chunkCount; chunk_id++) {\n    yield await held.get(chunk_id)! \u002F\u002F remis dans l'ordre à celui qui écrit le fichier\n    held.delete(chunk_id)\n    startNextChunks()\n}\n","app\u002Fcomposables\u002Fuse-download.ts",[39,387,388,410,414,432,487,502,509,532,564,569,587,595,603,615,622,634,642,664,670,675,680,688,727,754,771,779],{"__ignoreMap":37},[42,389,390,392,395,397,401,404,407],{"class":44,"line":45},[42,391,162],{"class":161},[42,393,394],{"class":165}," LOOKAHEAD ",[42,396,170],{"class":169},[42,398,400],{"class":399},"sbssI"," 2",[42,402,403],{"class":169}," *",[42,405,406],{"class":165}," BATCH_SIZE ",[42,408,409],{"class":155},"\u002F\u002F morceaux gardés à la fois, écriture comprise\n",[42,411,412],{"class":44,"line":51},[42,413,91],{"emptyLinePlaceholder":90},[42,415,416,419,422,425,429],{"class":44,"line":57},[42,417,418],{"class":161},"function",[42,420,421],{"class":177}," startNextChunks",[42,423,424],{"class":169},"():",[42,426,428],{"class":427},"sBMFI"," void",[42,430,431],{"class":169}," {\n",[42,433,434,437,439,442,445,448,451,454,457,459,462,464,467,469,472,474,477,479,482,485],{"class":44,"line":63},[42,435,436],{"class":173},"    while",[42,438,296],{"class":336},[42,440,441],{"class":169},"!",[42,443,444],{"class":165},"failed",[42,446,447],{"class":169}," &&",[42,449,450],{"class":165}," next",[42,452,453],{"class":169}," \u003C",[42,455,456],{"class":165}," chunkCount",[42,458,447],{"class":169},[42,460,461],{"class":165}," inFlight",[42,463,453],{"class":169},[42,465,466],{"class":165}," BATCH_SIZE",[42,468,447],{"class":169},[42,470,471],{"class":165}," held",[42,473,189],{"class":169},[42,475,476],{"class":165},"size",[42,478,453],{"class":169},[42,480,481],{"class":165}," LOOKAHEAD",[42,483,484],{"class":336},") ",[42,486,318],{"class":169},[42,488,489,492,495,497,499],{"class":44,"line":69},[42,490,491],{"class":161},"        const",[42,493,494],{"class":165}," chunk_id",[42,496,329],{"class":169},[42,498,450],{"class":165},[42,500,501],{"class":169},"++\n",[42,503,504,507],{"class":44,"line":75},[42,505,506],{"class":165},"        inFlight",[42,508,501],{"class":169},[42,510,511,513,516,518,521,523,526,528,530],{"class":44,"line":81},[42,512,491],{"class":161},[42,514,515],{"class":165}," chunk",[42,517,329],{"class":169},[42,519,520],{"class":177}," doDownload",[42,522,337],{"class":336},[42,524,525],{"class":165},"file_transfer",[42,527,348],{"class":169},[42,529,494],{"class":165},[42,531,215],{"class":336},[42,533,534,537,540,542,546,549,552,554,556,558,561],{"class":44,"line":87},[42,535,536],{"class":169},"            .",[42,538,539],{"class":177},"then",[42,541,337],{"class":336},[42,543,545],{"class":544},"sHdIc","res",[42,547,548],{"class":161}," =>",[42,550,551],{"class":177}," decryptWithIdentity",[42,553,337],{"class":336},[42,555,545],{"class":165},[42,557,348],{"class":169},[42,559,560],{"class":165}," session_private_key",[42,562,563],{"class":336},"))\n",[42,565,566],{"class":44,"line":94},[42,567,568],{"class":155},"        \u002F\u002F Un créneau libéré relance aussitôt un téléchargement\n",[42,570,571,574,576,578,580,583,585],{"class":44,"line":100},[42,572,573],{"class":165},"        chunk",[42,575,189],{"class":169},[42,577,539],{"class":177},[42,579,337],{"class":336},[42,581,582],{"class":169},"()",[42,584,548],{"class":161},[42,586,431],{"class":169},[42,588,589,592],{"class":44,"line":106},[42,590,591],{"class":165},"            inFlight",[42,593,594],{"class":169},"--;\n",[42,596,597,600],{"class":44,"line":111},[42,598,599],{"class":177},"            startNextChunks",[42,601,602],{"class":336},"()\n",[42,604,605,608,611,613],{"class":44,"line":117},[42,606,607],{"class":169},"        },",[42,609,610],{"class":169}," ()",[42,612,548],{"class":161},[42,614,431],{"class":169},[42,616,618,620],{"class":44,"line":617},14,[42,619,591],{"class":165},[42,621,594],{"class":169},[42,623,625,628,630],{"class":44,"line":624},15,[42,626,627],{"class":165},"            failed",[42,629,329],{"class":169},[42,631,633],{"class":632},"sfNiH"," true\n",[42,635,637,640],{"class":44,"line":636},16,[42,638,639],{"class":169},"        }",[42,641,215],{"class":336},[42,643,645,648,650,653,655,658,660,662],{"class":44,"line":644},17,[42,646,647],{"class":165},"        held",[42,649,189],{"class":169},[42,651,652],{"class":177},"set",[42,654,337],{"class":336},[42,656,657],{"class":165},"chunk_id",[42,659,348],{"class":169},[42,661,515],{"class":165},[42,663,215],{"class":336},[42,665,667],{"class":44,"line":666},18,[42,668,669],{"class":169},"    }\n",[42,671,673],{"class":44,"line":672},19,[42,674,367],{"class":169},[42,676,678],{"class":44,"line":677},20,[42,679,91],{"emptyLinePlaceholder":90},[42,681,683,686],{"class":44,"line":682},21,[42,684,685],{"class":177},"startNextChunks",[42,687,602],{"class":165},[42,689,691,693,695,698,701,703,706,709,711,714,716,718,720,723,725],{"class":44,"line":690},22,[42,692,291],{"class":173},[42,694,296],{"class":165},[42,696,697],{"class":161},"let",[42,699,700],{"class":165}," chunk_id ",[42,702,170],{"class":169},[42,704,705],{"class":399}," 0",[42,707,708],{"class":169},";",[42,710,700],{"class":165},[42,712,713],{"class":169},"\u003C",[42,715,456],{"class":165},[42,717,708],{"class":169},[42,719,494],{"class":165},[42,721,722],{"class":169},"++",[42,724,484],{"class":165},[42,726,318],{"class":169},[42,728,730,733,735,737,739,742,744,746,749,751],{"class":44,"line":729},23,[42,731,732],{"class":173},"    yield",[42,734,174],{"class":173},[42,736,471],{"class":165},[42,738,189],{"class":169},[42,740,741],{"class":177},"get",[42,743,337],{"class":336},[42,745,657],{"class":165},[42,747,748],{"class":336},")",[42,750,441],{"class":169},[42,752,753],{"class":155}," \u002F\u002F remis dans l'ordre à celui qui écrit le fichier\n",[42,755,757,760,762,765,767,769],{"class":44,"line":756},24,[42,758,759],{"class":165},"    held",[42,761,189],{"class":169},[42,763,764],{"class":177},"delete",[42,766,337],{"class":336},[42,768,657],{"class":165},[42,770,215],{"class":336},[42,772,774,777],{"class":44,"line":773},25,[42,775,776],{"class":177},"    startNextChunks",[42,778,602],{"class":336},[42,780,782],{"class":44,"line":781},26,[42,783,367],{"class":169},[13,785,786],{},"Un morceau lent ne bloque pas les autres : pendant qu'on l'attend, les créneaux libérés continuent de télécharger les\nsuivants, qui patientent déchiffrés en mémoire. Cette avance a toutefois un plafond, pour que la mémoire occupée ne\ndépende jamais de la taille du fichier.",[13,788,789],{},"Le déchiffrement vérifie aussi l'intégrité. age authentifie les données qu'il chiffre : si un morceau a été modifié,\nque ce soit sur le stockage, en transit ou par quelqu'un qui aurait accès à nos serveurs, le déchiffrement échoue au\nlieu de produire des données corrompues. Un fichier que vous obtenez est donc exactement celui qui a été envoyé, ou\nrien.",[20,791,793],{"id":792},"lenregistrement-là-où-les-navigateurs-diffèrent","L'enregistrement : là où les navigateurs diffèrent",[13,795,796],{},"Il reste à écrire le résultat sur votre disque. C'est la seule étape où votre navigateur change vraiment\nl'expérience, et elle mérite qu'on s'y arrête.",[13,798,799,800,805],{},"Sur un téléchargement ordinaire, le serveur envoie le fichier et le navigateur l'enregistre de lui-même. Ici, le\nfichier n'existe en clair que dans la page, et une page web n'a normalement aucun droit d'écrire sur votre disque. Deux\ncas se présentent, selon que le navigateur propose ou non l'API\n",[801,802,804],"external-link",{"url":803},"https:\u002F\u002Fdeveloper.mozilla.org\u002Ffr\u002Fdocs\u002FWeb\u002FAPI\u002FFile_System_API","File System"," :",[31,807,809],{"className":145,"code":808,"filename":385,"language":148,"meta":37,"style":37},"const supportsFSAccess = computed\u003Cboolean>(() => {\n    try {\n        return 'showDirectoryPicker' in window && 'showSaveFilePicker' in window\n    } catch {\n        return false\n    }\n})\n",[39,810,811,839,846,881,891,898,902],{"__ignoreMap":37},[42,812,813,815,818,820,823,825,828,831,833,835,837],{"class":44,"line":45},[42,814,162],{"class":161},[42,816,817],{"class":165}," supportsFSAccess ",[42,819,170],{"class":169},[42,821,822],{"class":177}," computed",[42,824,713],{"class":169},[42,826,827],{"class":427},"boolean",[42,829,830],{"class":169},">",[42,832,337],{"class":165},[42,834,582],{"class":169},[42,836,548],{"class":161},[42,838,431],{"class":169},[42,840,841,844],{"class":44,"line":51},[42,842,843],{"class":173},"    try",[42,845,431],{"class":169},[42,847,848,851,854,858,861,864,867,869,871,874,876,878],{"class":44,"line":57},[42,849,850],{"class":173},"        return",[42,852,853],{"class":169}," '",[42,855,857],{"class":856},"sfazB","showDirectoryPicker",[42,859,860],{"class":169},"'",[42,862,863],{"class":169}," in",[42,865,866],{"class":165}," window",[42,868,447],{"class":169},[42,870,853],{"class":169},[42,872,873],{"class":856},"showSaveFilePicker",[42,875,860],{"class":169},[42,877,863],{"class":169},[42,879,880],{"class":165}," window\n",[42,882,883,886,889],{"class":44,"line":63},[42,884,885],{"class":169},"    }",[42,887,888],{"class":173}," catch",[42,890,431],{"class":169},[42,892,893,895],{"class":44,"line":69},[42,894,850],{"class":173},[42,896,897],{"class":632}," false\n",[42,899,900],{"class":44,"line":75},[42,901,669],{"class":169},[42,903,904,907],{"class":44,"line":81},[42,905,906],{"class":169},"}",[42,908,215],{"class":165},[13,910,911],{},"À ce jour, seuls les navigateurs basés sur Chromium sur ordinateur la proposent : Chrome, Edge, Opera, Brave. Firefox\net Safari ne l'implémentent pas.",[13,913,914,918],{},[915,916,917],"strong",{},"Avec l'API",", le navigateur ouvre la fenêtre d'enregistrement de votre système, et vous choisissez un fichier ou un\ndossier. La page reçoit alors le droit d'écrire à cet endroit précis, et nulle part ailleurs, puis y écrit les données\ndéchiffrées. Pour un téléchargement groupé, vous choisissez un dossier une seule fois, et Retyc y recrée les\nsous-dossiers d'origine avant d'y écrire les fichiers un par un :",[31,920,922],{"className":145,"code":921,"filename":385,"language":148,"meta":37,"style":37},"const fileHandle = await getFileHandleFromPath(dirHandle, ft.name) \u002F\u002F crée \"contrats\u002F2026\u002F\" si besoin\nconst writable = await fileHandle.createWritable()\nfor await (const chunk of getChunkIterator(ft, session_private_key)) {\n    await chunk.stream().pipeTo(writable, {preventClose: true})\n}\nawait writable.close()\n",[39,923,924,954,975,1003,1045,1049],{"__ignoreMap":37},[42,925,926,928,931,933,935,938,941,943,946,948,951],{"class":44,"line":45},[42,927,162],{"class":161},[42,929,930],{"class":165}," fileHandle ",[42,932,170],{"class":169},[42,934,174],{"class":173},[42,936,937],{"class":177}," getFileHandleFromPath",[42,939,940],{"class":165},"(dirHandle",[42,942,348],{"class":169},[42,944,945],{"class":165}," ft",[42,947,189],{"class":169},[42,949,950],{"class":165},"name) ",[42,952,953],{"class":155},"\u002F\u002F crée \"contrats\u002F2026\u002F\" si besoin\n",[42,955,956,958,961,963,965,968,970,973],{"class":44,"line":51},[42,957,162],{"class":161},[42,959,960],{"class":165}," writable ",[42,962,170],{"class":169},[42,964,174],{"class":173},[42,966,967],{"class":165}," fileHandle",[42,969,189],{"class":169},[42,971,972],{"class":177},"createWritable",[42,974,602],{"class":165},[42,976,977,979,981,983,985,988,990,993,996,998,1001],{"class":44,"line":57},[42,978,291],{"class":173},[42,980,174],{"class":173},[42,982,296],{"class":165},[42,984,162],{"class":161},[42,986,987],{"class":165}," chunk ",[42,989,304],{"class":169},[42,991,992],{"class":177}," getChunkIterator",[42,994,995],{"class":165},"(ft",[42,997,348],{"class":169},[42,999,1000],{"class":165}," session_private_key)) ",[42,1002,318],{"class":169},[42,1004,1005,1008,1010,1012,1015,1017,1019,1022,1024,1027,1029,1032,1035,1038,1041,1043],{"class":44,"line":63},[42,1006,1007],{"class":173},"    await",[42,1009,515],{"class":165},[42,1011,189],{"class":169},[42,1013,1014],{"class":177},"stream",[42,1016,582],{"class":336},[42,1018,189],{"class":169},[42,1020,1021],{"class":177},"pipeTo",[42,1023,337],{"class":336},[42,1025,1026],{"class":165},"writable",[42,1028,348],{"class":169},[42,1030,1031],{"class":169}," {",[42,1033,1034],{"class":336},"preventClose",[42,1036,1037],{"class":169},":",[42,1039,1040],{"class":632}," true",[42,1042,906],{"class":169},[42,1044,215],{"class":336},[42,1046,1047],{"class":44,"line":69},[42,1048,367],{"class":169},[42,1050,1051,1054,1057,1059,1062],{"class":44,"line":75},[42,1052,1053],{"class":173},"await",[42,1055,1056],{"class":165}," writable",[42,1058,189],{"class":169},[42,1060,1061],{"class":177},"close",[42,1063,602],{"class":165},[1065,1066,1067],"blockquote",{},[13,1068,1069,1070,1073,1074,1076,1077,189],{},"Petite anecdote, lors de la bêta de Retyc, un utilisateur nous a signalé qu'il ne pouvait pas télécharger une centaine\nde fichiers d'un coup depuis une dataroom. Après investigation (non reproductible en local), il s'est avéré que le\nnavigateur remontait une ",[39,1071,1072],{},"SecurityError",". Dans la fonction ",[39,1075,857],{}," doit être appelée\n\"en réponse directe à un clic de l'utilisateur\", et le téléchargement groupé ne respectait pas cette règle\n(l'appel était fait après le scan complet des fichiers). Ce point a été corrigé dans la version ",[39,1078,1079],{},"1.0.17",[13,1081,1082],{},"Chaque morceau est écrit sur le disque dès que son tour arrive, pendant que les suivants se téléchargent. Au plus huit\nmorceaux sont gardés à la fois, en cours de téléchargement, en attente ou en cours d'écriture : la mémoire occupée\nreste au plus de l'ordre de 128 Mo (8 chunks x 8 Mo x 2 versions : en clair et chiffré),\nque le fichier fasse 100 Mo ou 50 Go.",[13,1084,1085,1088,1089,189],{},[915,1086,1087],{},"Sans l'API",", une page n'a qu'un moyen de produire un fichier : l'assembler entièrement en mémoire, puis simuler un\nclic sur un lien de téléchargement. Pour plusieurs fichiers, le navigateur ne sait pas créer de dossiers : Retyc les\nemballe dans une archive zip, construite dans la page avec ",[801,1090,1092],{"url":1091},"https:\u002F\u002Fstuk.github.io\u002Fjszip\u002F","JSZip",[13,1094,1095],{},"Ce mode de repli a un coût qui grandit avec la taille des fichiers : la mémoire vive. Tant que le fichier n'est pas\nentièrement déchiffré et assemblé, rien ne peut être écrit sur le disque, si bien que tout le contenu reste dans\nl'onglet, sous forme de morceaux déchiffrés puis d'un grand blob qui les réunit. Pour un téléchargement groupé, c'est\npire : tous les fichiers du transfert s'accumulent avant que l'archive soit générée, et l'archive elle-même vient\ns'ajouter par-dessus. Quelques centaines de mégaoctets passent sans que vous le remarquiez. Plusieurs gigaoctets, en\nrevanche, peuvent occuper l'essentiel de la mémoire d'un ordinateur portable ordinaire : la machine ralentit, et le\nnavigateur finit parfois par fermer l'onglet avant la fin, en perdant tout le travail déjà fait. Pour un gros\ntransfert, le mode de repli n'est donc pas seulement plus lent à livrer ses fichiers, il peut ne pas aboutir du tout.",[13,1097,1098],{},"La différence se sent surtout sur les gros volumes :",[1100,1101,1102,1117],"table",{},[1103,1104,1105],"thead",{},[1106,1107,1108,1111,1114],"tr",{},[1109,1110],"th",{},[1109,1112,1113],{},"Avec l'API (Chrome, Edge...)",[1109,1115,1116],{},"Sans l'API (Firefox, Safari)",[1118,1119,1120,1132,1143,1154,1165],"tbody",{},[1106,1121,1122,1126,1129],{},[1123,1124,1125],"td",{},"Plusieurs fichiers",[1123,1127,1128],{},"Un dossier, arborescence recréée",[1123,1130,1131],{},"Une archive zip à décompresser",[1106,1133,1134,1137,1140],{},[1123,1135,1136],{},"Mémoire utilisée",[1123,1138,1139],{},"Environ 128 Mo, quelle que soit la taille",[1123,1141,1142],{},"Tout le fichier, ou tout le transfert plus l'archive",[1106,1144,1145,1148,1151],{},[1123,1146,1147],{},"Taille maximale d'un envoi groupé",[1123,1149,1150],{},"Espace disque disponible",[1123,1152,1153],{},"4 Go et 65 535 fichiers par archive",[1106,1155,1156,1159,1162],{},[1123,1157,1158],{},"Premier fichier disponible",[1123,1160,1161],{},"Dès qu'il est écrit",[1123,1163,1164],{},"Une fois toute l'archive générée",[1106,1166,1167,1170,1173],{},[1123,1168,1169],{},"Emplacement",[1123,1171,1172],{},"Choisi par vous",[1123,1174,1175],{},"Dossier « Téléchargements »",[13,1177,1178],{},"La limite de 4 Go vient du format : JSZip ne sait pas écrire Zip64, l'extension qui permet de la dépasser. Et comme\nl'archive doit être entièrement construite en mémoire avant d'être proposée, un transfert de plusieurs gigaoctets peut\népuiser la mémoire de l'onglet avant la fin. C'est pourquoi la fenêtre de téléchargement groupé sélectionne le mode\n« Moderne » quand il est disponible, et signale que le zip n'est pas recommandé pour les gros fichiers.",[13,1180,1181],{},"Pour le téléchargement d'un seul fichier, Retyc choisit automatiquement l'utilisation de l'API File System\nsi elle est disponible. Tandis que pour un téléchargement groupé, l'interface web de Retyc propose\nle mode \"Moderne\" quand il est disponible ou le mode « Zip » le cas échéant.",[13,1183,1184],{},"Si vous recevez régulièrement des dossiers volumineux, un navigateur basé sur Chromium vous évitera ces limites. Pour\nle reste, le mode zip fait très bien l'affaire. Dans les deux cas, le déchiffrement a lieu sur votre machine.",[20,1186,1188],{"id":1187},"un-nom-de-fichier-est-une-donnée-étrangère","Un nom de fichier est une donnée étrangère",[13,1190,1191,1192,1194],{},"Le chemin ",[39,1193,373],{}," a été choisi par l'expéditeur, puis chiffré. Une fois déchiffré, c'est une chaîne\nfournie par un tiers qui va décider où un fichier est écrit sur votre disque. Avant d'écrire quoi que ce soit, Retyc\nrefuse donc tout chemin qui tenterait de sortir du dossier choisi :",[31,1196,1198],{"className":145,"code":1197,"filename":385,"language":148,"meta":37,"style":37},"function assertSafePath(name: string): void {\n    if (name.startsWith('\u002F') || name.split('\u002F').some(seg => seg === '..')) {\n        throw new Error(`Unsafe file path rejected: ${name}`)\n    }\n}\n",[39,1199,1200,1224,1300,1329,1333],{"__ignoreMap":37},[42,1201,1202,1204,1207,1209,1212,1214,1217,1220,1222],{"class":44,"line":45},[42,1203,418],{"class":161},[42,1205,1206],{"class":177}," assertSafePath",[42,1208,337],{"class":169},[42,1210,1211],{"class":544},"name",[42,1213,1037],{"class":169},[42,1215,1216],{"class":427}," string",[42,1218,1219],{"class":169},"):",[42,1221,428],{"class":427},[42,1223,431],{"class":169},[42,1225,1226,1229,1231,1233,1235,1238,1240,1242,1245,1247,1249,1252,1255,1257,1260,1262,1264,1266,1268,1270,1272,1275,1277,1280,1282,1285,1288,1290,1293,1295,1298],{"class":44,"line":51},[42,1227,1228],{"class":173},"    if",[42,1230,296],{"class":336},[42,1232,1211],{"class":165},[42,1234,189],{"class":169},[42,1236,1237],{"class":177},"startsWith",[42,1239,337],{"class":336},[42,1241,860],{"class":169},[42,1243,1244],{"class":856},"\u002F",[42,1246,860],{"class":169},[42,1248,484],{"class":336},[42,1250,1251],{"class":169},"||",[42,1253,1254],{"class":165}," name",[42,1256,189],{"class":169},[42,1258,1259],{"class":177},"split",[42,1261,337],{"class":336},[42,1263,860],{"class":169},[42,1265,1244],{"class":856},[42,1267,860],{"class":169},[42,1269,748],{"class":336},[42,1271,189],{"class":169},[42,1273,1274],{"class":177},"some",[42,1276,337],{"class":336},[42,1278,1279],{"class":544},"seg",[42,1281,548],{"class":161},[42,1283,1284],{"class":165}," seg",[42,1286,1287],{"class":169}," ===",[42,1289,853],{"class":169},[42,1291,1292],{"class":856},"..",[42,1294,860],{"class":169},[42,1296,1297],{"class":336},")) ",[42,1299,318],{"class":169},[42,1301,1302,1305,1308,1311,1313,1316,1319,1322,1324,1327],{"class":44,"line":57},[42,1303,1304],{"class":173},"        throw",[42,1306,1307],{"class":169}," new",[42,1309,1310],{"class":177}," Error",[42,1312,337],{"class":336},[42,1314,1315],{"class":169},"`",[42,1317,1318],{"class":856},"Unsafe file path rejected: ",[42,1320,1321],{"class":169},"${",[42,1323,1211],{"class":165},[42,1325,1326],{"class":169},"}`",[42,1328,215],{"class":336},[42,1330,1331],{"class":44,"line":63},[42,1332,669],{"class":169},[42,1334,1335],{"class":44,"line":69},[42,1336,367],{"class":169},[13,1338,1339],{},"L'API du navigateur interdit déjà de remonter au-dessus du dossier accordé. Nous ne voulons pas pour autant dépendre\nd'un seul garde-fou quand la donnée vient de quelqu'un d'autre.",[13,1341,1342],{},"Pour la même raison, la fenêtre de sélection ne s'ouvre qu'en réponse directe à votre clic : les navigateurs refusent\nqu'une page décide seule, au milieu d'un traitement, d'écrire sur votre disque. Le dossier vous est donc demandé tout\nde suite, avant même de lister les fichiers. C'est vous, et pas la page, qui décidez de l'endroit où elle a le droit\nd'écrire.",[20,1344,1346],{"id":1345},"pour-aller-plus-loin","Pour aller plus loin",[13,1348,1349,1350,1354],{},"Le navigateur reste un environnement contraint. Pour les très gros volumes, les téléchargements automatisés ou les\nserveurs sans interface graphique, notre ",[801,1351,1353],{"url":1352},"https:\u002F\u002Fgithub.com\u002Fretyc\u002Fretyc-cli","CLI",", publié sous\nlicence MIT, récupère et déchiffre les transferts sans passer par un navigateur, ni par ses fenêtres de sélection ou\nses limites de mémoire d'onglet. Il applique le même format, et son code est lisible par tous.",[13,1356,1357,1358,1360,1361,1365],{},"Pour l'autre moitié du trajet, clés post-quantiques comprises,\n",[132,1359,135],{"href":134},"\ndétaille ce qui se passe avant que le premier octet ne quitte la machine de l'expéditeur. Et pour une description\ncomplète de l'architecture et de ses limites, le ",[132,1362,1364],{"href":1363},"\u002Ffr\u002Fresources\u002Fwhite-paper","livre blanc"," est en accès libre.",[1367,1368,1369],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":37,"searchDepth":51,"depth":51,"links":1371},[1372,1373,1374,1375,1376,1377,1378],{"id":22,"depth":51,"text":23},{"id":126,"depth":51,"text":127},{"id":276,"depth":51,"text":277},{"id":377,"depth":51,"text":378},{"id":792,"depth":51,"text":793},{"id":1187,"depth":51,"text":1188},{"id":1345,"depth":51,"text":1346},"Tech","2026-10-05","Ouvrir le lien, déverrouiller le transfert, déchiffrer les fichiers, les enregistrer : côté destinataire, tout se passe dans le navigateur. Ce qu'il fait à chaque étape, et pourquoi Chrome et Firefox ne vous proposent pas la même chose.","md",null,{},"\u002Fblog\u002Ffr\u002Freceiving-an-encrypted-transfer",{"title":1387,"description":1388,"ogTitle":5,"ogDescription":1389},"Télécharger un fichier chiffré de bout en bout dans le navigateur : ce qui se passe vraiment","Comment Retyc livre un transfert chiffré à son destinataire : contrôle d'accès, passphrase et scrypt, déchiffrement par morceaux, intégrité, enregistrement sur le disque selon le navigateur, consommation mémoire des gros fichiers. Avec le code.","Le serveur ne peut pas vous rendre vos fichiers : c'est votre navigateur qui les déchiffre. Voici comment, étape par étape.",{"loc":1385},"blog\u002Ffr\u002Freceiving-an-encrypted-transfer",[1393,1394,1395,1396,1397],"chiffrement de bout en bout","téléchargement","navigateur","File System Access API","TypeScript","2s8RI0qRpwkxU3KS3ROOn-N0jElHzU9ob6hI-5bItF8",[1383,1400],{"title":1401,"path":1402,"stem":1403,"description":1404,"children":-1},"Pentest par IA : notre retour d'expérience","\u002Fblog\u002Ffr\u002Fai-pentest","blog\u002Ffr\u002Fai-pentest","Grâce au Cyber Verification Portal d'Anthropic, nous avons fait un pentest de Retyc par Claude, sur notre environnement de développement."]