[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"BLOG_POST_fr_anatomy-of-an-encrypted-upload":3,"BLOG_SURROUND_fr_anatomy-of-an-encrypted-upload":1824},{"id":4,"title":5,"authors":6,"body":9,"category":1805,"date":1806,"description":1807,"extension":1808,"image":1809,"meta":1810,"navigation":270,"path":1811,"seo":1812,"sitemap":1816,"stem":1817,"tags":1818,"__hash__":1823},"blog\u002Fblog\u002Ffr\u002Fanatomy-of-an-encrypted-upload.md","Anatomie d'un upload chiffré dans le navigateur",[7],{"name":8},"Emilien Mantel",{"type":10,"value":11,"toc":1793},"minimark",[12,21,24,29,42,187,202,205,209,212,339,342,345,452,455,459,466,676,679,683,690,1116,1119,1122,1133,1137,1145,1212,1215,1379,1395,1399,1402,1405,1412,1566,1569,1573,1576,1628,1638,1642,1645,1726,1729,1733,1736,1757,1760,1764,1776,1789],[13,14,15,16,20],"p",{},"Vous glissez un fichier de 3 Go dans la page, vous choisissez un destinataire, vous cliquez sur « Envoyer ». La barre de\nprogression démarre presque aussitôt. Entre les deux, le navigateur a déjà généré une paire de clés, chiffré cette clé\npour chaque destinataire, chiffré le nom du fichier, et commencé à découper le reste en morceaux qu'il chiffre un par un\navant de les envoyer. Le serveur, lui, ne reçoit jamais que des fichiers ",[17,18,19],"code",{},".age",".",[13,22,23],{},"Cet article suit ce trajet dans l'ordre, avec le code qui le fait. Les extraits viennent du front de Retyc (Nuxt,\nTypeScript) et de l'API (FastAPI). Ils sont raccourcis pour la lecture, et chacun indique le fichier d'origine.",[25,26,28],"h2",{"id":27},"une-paire-de-clés-pour-chaque-transfert","Une paire de clés pour chaque transfert",[13,30,31,32,37,38,41],{},"Tout repose sur ",[33,34,36],"external-link",{"url":35},"https:\u002F\u002Fage-encryption.org\u002Fv1","age",", un format de chiffrement de fichiers à la\nspécification publique,\net sur son implémentation JavaScript, ",[17,39,40],{},"age-encryption",". Au début de chaque transfert, le navigateur crée une identité\nage qui ne servira qu'à ce transfert :",[43,44,50],"pre",{"className":45,"code":46,"filename":47,"language":48,"meta":49,"style":49},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","async function generateSessionIdentity()\n{\n    const {generateHybridIdentity, identityToRecipient} = await loadAge()\n    const identity = await generateHybridIdentity()\n    return {\n        public_key: await identityToRecipient(identity),\n        private_key: identity\n    }\n}\n","app\u002Fshared\u002Futils\u002Fcrypto-core.ts","ts","",[17,51,52,72,78,113,130,139,164,175,181],{"__ignoreMap":49},[53,54,57,61,64,68],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"spNyl","async",[53,62,63],{"class":59}," function",[53,65,67],{"class":66},"s2Zo4"," generateSessionIdentity",[53,69,71],{"class":70},"sMK4o","()\n",[53,73,75],{"class":55,"line":74},2,[53,76,77],{"class":70},"{\n",[53,79,81,84,87,91,94,97,100,103,107,110],{"class":55,"line":80},3,[53,82,83],{"class":59},"    const",[53,85,86],{"class":70}," {",[53,88,90],{"class":89},"sTEyZ","generateHybridIdentity",[53,92,93],{"class":70},",",[53,95,96],{"class":89}," identityToRecipient",[53,98,99],{"class":70},"}",[53,101,102],{"class":70}," =",[53,104,106],{"class":105},"s7zQu"," await",[53,108,109],{"class":66}," loadAge",[53,111,71],{"class":112},"swJcz",[53,114,116,118,121,123,125,128],{"class":55,"line":115},4,[53,117,83],{"class":59},[53,119,120],{"class":89}," identity",[53,122,102],{"class":70},[53,124,106],{"class":105},[53,126,127],{"class":66}," generateHybridIdentity",[53,129,71],{"class":112},[53,131,133,136],{"class":55,"line":132},5,[53,134,135],{"class":105},"    return",[53,137,138],{"class":70}," {\n",[53,140,142,145,148,150,152,155,158,161],{"class":55,"line":141},6,[53,143,144],{"class":112},"        public_key",[53,146,147],{"class":70},":",[53,149,106],{"class":105},[53,151,96],{"class":66},[53,153,154],{"class":112},"(",[53,156,157],{"class":89},"identity",[53,159,160],{"class":112},")",[53,162,163],{"class":70},",\n",[53,165,167,170,172],{"class":55,"line":166},7,[53,168,169],{"class":112},"        private_key",[53,171,147],{"class":70},[53,173,174],{"class":89}," identity\n",[53,176,178],{"class":55,"line":177},8,[53,179,180],{"class":70},"    }\n",[53,182,184],{"class":55,"line":183},9,[53,185,186],{"class":70},"}\n",[13,188,189,192,193,197,198,201],{},[17,190,191],{},"generateHybridIdentity()"," produit une paire ",[194,195,196],"strong",{},"hybride"," : elle combine X25519, l'échange de clés elliptique classique,\net ML-KEM-768, le mécanisme d'encapsulation standardisé par le NIST pour résister aux ordinateurs quantiques. Pour lire\nun fichier, il faut casser les deux. La clé publique qui en sort commence par ",[17,199,200],{},"age1pq1"," et fait plus d'un millier de\ncaractères, contre une soixantaine pour une clé age classique. Ce préfixe servira plus loin.",[13,203,204],{},"Pourquoi une clé par transfert plutôt que la clé de l'expéditeur directement ? Parce qu'un transfert a plusieurs\nlecteurs (vous, vos destinataires, parfois une passphrase), et qu'on ne veut chiffrer les 3 Go qu'une seule fois. La clé\nde session chiffre le contenu. Le reste du travail consiste à distribuer cette clé.",[25,206,208],{"id":207},"emballer-la-clé-pour-chaque-lecteur","Emballer la clé pour chaque lecteur",[13,210,211],{},"C'est le principe du chiffrement d'enveloppe. Les données sont chiffrées avec une clé, et c'est cette clé qu'on chiffre\nensuite pour chaque personne autorisée. Chez nous, ça tient en quelques lignes :",[43,213,216],{"className":45,"code":214,"filename":215,"language":48,"meta":49,"style":49},"\u002F\u002F Vous gardez l'accès à votre propre transfert\nif (encryptWithMyKey) {\n    public_keys.push(keyPair.value.public_key)\n}\n\nconst session_identity = await generateSessionIdentity()\n\n\u002F\u002F La clé privée de session, chiffrée pour tous les destinataires d'un coup\nconst session_private_key_enc = await encryptStringWithRecipients(\n    session_identity.private_key,\n    public_keys\n)\n","app\u002Fcomposables\u002Fuse-upload-form.ts",[17,217,218,224,234,262,266,272,289,293,298,315,328,334],{"__ignoreMap":49},[53,219,220],{"class":55,"line":56},[53,221,223],{"class":222},"sHwdD","\u002F\u002F Vous gardez l'accès à votre propre transfert\n",[53,225,226,229,232],{"class":55,"line":74},[53,227,228],{"class":105},"if",[53,230,231],{"class":89}," (encryptWithMyKey) ",[53,233,77],{"class":70},[53,235,236,239,241,244,246,249,251,254,256,259],{"class":55,"line":80},[53,237,238],{"class":89},"    public_keys",[53,240,20],{"class":70},[53,242,243],{"class":66},"push",[53,245,154],{"class":112},[53,247,248],{"class":89},"keyPair",[53,250,20],{"class":70},[53,252,253],{"class":89},"value",[53,255,20],{"class":70},[53,257,258],{"class":89},"public_key",[53,260,261],{"class":112},")\n",[53,263,264],{"class":55,"line":115},[53,265,186],{"class":70},[53,267,268],{"class":55,"line":132},[53,269,271],{"emptyLinePlaceholder":270},true,"\n",[53,273,274,277,280,283,285,287],{"class":55,"line":141},[53,275,276],{"class":59},"const",[53,278,279],{"class":89}," session_identity ",[53,281,282],{"class":70},"=",[53,284,106],{"class":105},[53,286,67],{"class":66},[53,288,71],{"class":89},[53,290,291],{"class":55,"line":166},[53,292,271],{"emptyLinePlaceholder":270},[53,294,295],{"class":55,"line":177},[53,296,297],{"class":222},"\u002F\u002F La clé privée de session, chiffrée pour tous les destinataires d'un coup\n",[53,299,300,302,305,307,309,312],{"class":55,"line":183},[53,301,276],{"class":59},[53,303,304],{"class":89}," session_private_key_enc ",[53,306,282],{"class":70},[53,308,106],{"class":105},[53,310,311],{"class":66}," encryptStringWithRecipients",[53,313,314],{"class":89},"(\n",[53,316,318,321,323,326],{"class":55,"line":317},10,[53,319,320],{"class":89},"    session_identity",[53,322,20],{"class":70},[53,324,325],{"class":89},"private_key",[53,327,163],{"class":70},[53,329,331],{"class":55,"line":330},11,[53,332,333],{"class":89},"    public_keys\n",[53,335,337],{"class":55,"line":336},12,[53,338,261],{"class":89},[13,340,341],{},"age accepte plusieurs destinataires dans un même fichier : chacun a sa propre entrée dans l'en-tête, et n'importe\nlaquelle suffit à retrouver la clé de fichier. Un destinataire qui a un compte ouvre donc l'enveloppe avec sa clé\nprivée, que son navigateur a déverrouillée avec sa passphrase, sans jamais l'envoyer en clair.",[13,343,344],{},"Un destinataire sans compte n'a pas de clé publique. Pour lui, le navigateur crée une deuxième paire, éphémère, dont la\nclé privée est protégée par la passphrase du transfert :",[43,346,348],{"className":45,"code":347,"filename":215,"language":48,"meta":49,"style":49},"if (passphrase.length > 0) {\n    \u002F\u002F Clé privée éphémère chiffrée avec la passphrase (scrypt)\n    const ephemeralKeypair = await createAgeIdentityPair(passphrase)\n\n    session_private_key_enc_for_passphrase = await encryptStringWithRecipients(\n        session_identity.private_key,\n        [ephemeralKeypair.public_key]\n    )\n}\n",[17,349,350,374,379,400,404,417,428,443,448],{"__ignoreMap":49},[53,351,352,354,357,359,362,365,369,372],{"class":55,"line":56},[53,353,228],{"class":105},[53,355,356],{"class":89}," (passphrase",[53,358,20],{"class":70},[53,360,361],{"class":89},"length ",[53,363,364],{"class":70},">",[53,366,368],{"class":367},"sbssI"," 0",[53,370,371],{"class":89},") ",[53,373,77],{"class":70},[53,375,376],{"class":55,"line":74},[53,377,378],{"class":222},"    \u002F\u002F Clé privée éphémère chiffrée avec la passphrase (scrypt)\n",[53,380,381,383,386,388,390,393,395,398],{"class":55,"line":80},[53,382,83],{"class":59},[53,384,385],{"class":89}," ephemeralKeypair",[53,387,102],{"class":70},[53,389,106],{"class":105},[53,391,392],{"class":66}," createAgeIdentityPair",[53,394,154],{"class":112},[53,396,397],{"class":89},"passphrase",[53,399,261],{"class":112},[53,401,402],{"class":55,"line":115},[53,403,271],{"emptyLinePlaceholder":270},[53,405,406,409,411,413,415],{"class":55,"line":132},[53,407,408],{"class":89},"    session_private_key_enc_for_passphrase",[53,410,102],{"class":70},[53,412,106],{"class":105},[53,414,311],{"class":66},[53,416,314],{"class":112},[53,418,419,422,424,426],{"class":55,"line":141},[53,420,421],{"class":89},"        session_identity",[53,423,20],{"class":70},[53,425,325],{"class":89},[53,427,163],{"class":70},[53,429,430,433,436,438,440],{"class":55,"line":166},[53,431,432],{"class":112},"        [",[53,434,435],{"class":89},"ephemeralKeypair",[53,437,20],{"class":70},[53,439,258],{"class":89},[53,441,442],{"class":112},"]\n",[53,444,445],{"class":55,"line":177},[53,446,447],{"class":112},"    )\n",[53,449,450],{"class":55,"line":183},[53,451,186],{"class":70},[13,453,454],{},"La passphrase passe par scrypt, une fonction de dérivation volontairement lente et gourmande en mémoire. Chaque\ntentative de devinette coûte cher, même à quelqu'un qui aurait mis la main sur l'enveloppe. La passphrase elle-même ne\nquitte jamais le navigateur : vous la transmettez à votre destinataire par un autre canal.",[25,456,458],{"id":457},"les-noms-de-fichiers-aussi","Les noms de fichiers aussi",[13,460,461,462,465],{},"Un contenu chiffré sous un nom en clair, comme ",[17,463,464],{},"2026-pentest-application-xyz.pdf",", en dit déjà beaucoup. Avant d'envoyer le\nmoindre octet de contenu, le navigateur déclare donc chaque fichier à l'API avec un nom et un type MIME déjà chiffrés\npour la clé de session :",[43,467,470],{"className":45,"code":468,"filename":469,"language":48,"meta":49,"style":49},"\u002F\u002F Pour un dossier, webkitRelativePath donne le chemin complet : \"contrats\u002F2026\u002Favenant.pdf\"\nconst fileName = file.webkitRelativePath || file.name\n\nconst [name_enc, type_enc] = await Promise.all([\n    encryptStringWithRecipients(fileName, [session_public_key]),\n    encryptStringWithRecipients(file.type, [session_public_key])\n])\n\nawait $retyc('\u002Fshare\u002F{share_id}\u002Ffile', {\n    method: 'POST',\n    path: {share_id},\n    body: {name_enc, type_enc, original_size: file.size},\n})\n","app\u002Fcomposables\u002Fuse-share.ts",[17,471,472,477,504,508,542,557,574,579,583,606,623,638,669],{"__ignoreMap":49},[53,473,474],{"class":55,"line":56},[53,475,476],{"class":222},"\u002F\u002F Pour un dossier, webkitRelativePath donne le chemin complet : \"contrats\u002F2026\u002Favenant.pdf\"\n",[53,478,479,481,484,486,489,491,494,497,499,501],{"class":55,"line":74},[53,480,276],{"class":59},[53,482,483],{"class":89}," fileName ",[53,485,282],{"class":70},[53,487,488],{"class":89}," file",[53,490,20],{"class":70},[53,492,493],{"class":89},"webkitRelativePath ",[53,495,496],{"class":70},"||",[53,498,488],{"class":89},[53,500,20],{"class":70},[53,502,503],{"class":89},"name\n",[53,505,506],{"class":55,"line":80},[53,507,271],{"emptyLinePlaceholder":270},[53,509,510,512,515,518,520,523,526,528,530,534,536,539],{"class":55,"line":115},[53,511,276],{"class":59},[53,513,514],{"class":70}," [",[53,516,517],{"class":89},"name_enc",[53,519,93],{"class":70},[53,521,522],{"class":89}," type_enc",[53,524,525],{"class":70},"]",[53,527,102],{"class":70},[53,529,106],{"class":105},[53,531,533],{"class":532},"sBMFI"," Promise",[53,535,20],{"class":70},[53,537,538],{"class":66},"all",[53,540,541],{"class":89},"([\n",[53,543,544,547,550,552,555],{"class":55,"line":132},[53,545,546],{"class":66},"    encryptStringWithRecipients",[53,548,549],{"class":89},"(fileName",[53,551,93],{"class":70},[53,553,554],{"class":89}," [session_public_key])",[53,556,163],{"class":70},[53,558,559,561,564,566,569,571],{"class":55,"line":141},[53,560,546],{"class":66},[53,562,563],{"class":89},"(file",[53,565,20],{"class":70},[53,567,568],{"class":89},"type",[53,570,93],{"class":70},[53,572,573],{"class":89}," [session_public_key])\n",[53,575,576],{"class":55,"line":166},[53,577,578],{"class":89},"])\n",[53,580,581],{"class":55,"line":177},[53,582,271],{"emptyLinePlaceholder":270},[53,584,585,588,591,593,596,600,602,604],{"class":55,"line":183},[53,586,587],{"class":105},"await",[53,589,590],{"class":66}," $retyc",[53,592,154],{"class":89},[53,594,595],{"class":70},"'",[53,597,599],{"class":598},"sfazB","\u002Fshare\u002F{share_id}\u002Ffile",[53,601,595],{"class":70},[53,603,93],{"class":70},[53,605,138],{"class":70},[53,607,608,611,613,616,619,621],{"class":55,"line":317},[53,609,610],{"class":112},"    method",[53,612,147],{"class":70},[53,614,615],{"class":70}," '",[53,617,618],{"class":598},"POST",[53,620,595],{"class":70},[53,622,163],{"class":70},[53,624,625,628,630,632,635],{"class":55,"line":330},[53,626,627],{"class":112},"    path",[53,629,147],{"class":70},[53,631,86],{"class":70},[53,633,634],{"class":89},"share_id",[53,636,637],{"class":70},"},\n",[53,639,640,643,645,647,649,651,653,655,658,660,662,664,667],{"class":55,"line":336},[53,641,642],{"class":112},"    body",[53,644,147],{"class":70},[53,646,86],{"class":70},[53,648,517],{"class":89},[53,650,93],{"class":70},[53,652,522],{"class":89},[53,654,93],{"class":70},[53,656,657],{"class":112}," original_size",[53,659,147],{"class":70},[53,661,488],{"class":89},[53,663,20],{"class":70},[53,665,666],{"class":89},"size",[53,668,637],{"class":70},[53,670,672,674],{"class":55,"line":671},13,[53,673,99],{"class":70},[53,675,261],{"class":89},[13,677,678],{},"Quand on envoie un dossier, l'arborescence complète est chiffrée avec le nom. L'API voit passer un fichier, sa taille,\net c'est tout.",[25,680,682],{"id":681},"des-morceaux-de-8-mo-chiffrés-un-par-un","Des morceaux de 8 Mo, chiffrés un par un",[13,684,685,686,689],{},"Charger 3 Go en mémoire pour les chiffrer d'un bloc ferait tomber l'onglet bien avant la fin. Le fichier est donc\ndécoupé en morceaux avec ",[17,687,688],{},"Blob.slice()",", qui ne lit rien tant qu'on ne le lui demande pas. Chaque morceau est chiffré\nséparément, et devient un fichier age complet et autonome :",[43,691,694],{"className":45,"code":692,"filename":693,"language":48,"meta":49,"style":49},"const chunkCount = Math.ceil(file.size \u002F CHUNK_SIZE) \u002F\u002F 8 Mo par défaut\n\nfor (let chunkId = 0; chunkId \u003C chunkCount; chunkId++) {\n    tasks.push((async () => {\n        \u002F\u002F Chiffrer : on ne lit que les 8 Mo de ce morceau\n        const encryptedBlob = await limitEncrypt(() => {\n            const start = chunkId * CHUNK_SIZE\n            const end = Math.min(start + CHUNK_SIZE, file.size)\n            return encryptWithRecipient(file.slice(start, end), session_public_key)\n        })\n\n        \u002F\u002F Envoyer tout de suite, puis laisser le ramasse-miettes libérer le blob\n        const formData = new FormData()\n        formData.append('upload_file', encryptedBlob, 'chunk.age')\n        await doUpload(file_info, chunkId, formData, onChunkProgress, abortController.signal)\n    })().catch(err => {\n        cancelled = true\n        abortController.abort() \u002F\u002F coupe les requêtes encore en vol\n        throw err\n    }))\n}\n\nawait Promise.all(tasks)\n","app\u002Fcomposables\u002Fuse-upload.ts",[17,695,696,729,733,774,796,801,825,843,880,915,922,926,931,948,983,1022,1046,1058,1075,1084,1092,1097,1102],{"__ignoreMap":49},[53,697,698,700,703,705,708,710,713,715,717,720,723,726],{"class":55,"line":56},[53,699,276],{"class":59},[53,701,702],{"class":89}," chunkCount ",[53,704,282],{"class":70},[53,706,707],{"class":89}," Math",[53,709,20],{"class":70},[53,711,712],{"class":66},"ceil",[53,714,563],{"class":89},[53,716,20],{"class":70},[53,718,719],{"class":89},"size ",[53,721,722],{"class":70},"\u002F",[53,724,725],{"class":89}," CHUNK_SIZE) ",[53,727,728],{"class":222},"\u002F\u002F 8 Mo par défaut\n",[53,730,731],{"class":55,"line":74},[53,732,271],{"emptyLinePlaceholder":270},[53,734,735,738,741,744,747,749,751,754,756,759,762,764,767,770,772],{"class":55,"line":80},[53,736,737],{"class":105},"for",[53,739,740],{"class":89}," (",[53,742,743],{"class":59},"let",[53,745,746],{"class":89}," chunkId ",[53,748,282],{"class":70},[53,750,368],{"class":367},[53,752,753],{"class":70},";",[53,755,746],{"class":89},[53,757,758],{"class":70},"\u003C",[53,760,761],{"class":89}," chunkCount",[53,763,753],{"class":70},[53,765,766],{"class":89}," chunkId",[53,768,769],{"class":70},"++",[53,771,371],{"class":89},[53,773,77],{"class":70},[53,775,776,779,781,783,786,788,791,794],{"class":55,"line":115},[53,777,778],{"class":89},"    tasks",[53,780,20],{"class":70},[53,782,243],{"class":66},[53,784,785],{"class":112},"((",[53,787,60],{"class":59},[53,789,790],{"class":70}," ()",[53,792,793],{"class":59}," =>",[53,795,138],{"class":70},[53,797,798],{"class":55,"line":132},[53,799,800],{"class":222},"        \u002F\u002F Chiffrer : on ne lit que les 8 Mo de ce morceau\n",[53,802,803,806,809,811,813,816,818,821,823],{"class":55,"line":141},[53,804,805],{"class":59},"        const",[53,807,808],{"class":89}," encryptedBlob",[53,810,102],{"class":70},[53,812,106],{"class":105},[53,814,815],{"class":66}," limitEncrypt",[53,817,154],{"class":112},[53,819,820],{"class":70},"()",[53,822,793],{"class":59},[53,824,138],{"class":70},[53,826,827,830,833,835,837,840],{"class":55,"line":166},[53,828,829],{"class":59},"            const",[53,831,832],{"class":89}," start",[53,834,102],{"class":70},[53,836,766],{"class":89},[53,838,839],{"class":70}," *",[53,841,842],{"class":89}," CHUNK_SIZE\n",[53,844,845,847,850,852,854,856,859,861,864,867,870,872,874,876,878],{"class":55,"line":177},[53,846,829],{"class":59},[53,848,849],{"class":89}," end",[53,851,102],{"class":70},[53,853,707],{"class":89},[53,855,20],{"class":70},[53,857,858],{"class":66},"min",[53,860,154],{"class":112},[53,862,863],{"class":89},"start",[53,865,866],{"class":70}," +",[53,868,869],{"class":89}," CHUNK_SIZE",[53,871,93],{"class":70},[53,873,488],{"class":89},[53,875,20],{"class":70},[53,877,666],{"class":89},[53,879,261],{"class":112},[53,881,882,885,888,890,893,895,898,900,902,904,906,908,910,913],{"class":55,"line":183},[53,883,884],{"class":105},"            return",[53,886,887],{"class":66}," encryptWithRecipient",[53,889,154],{"class":112},[53,891,892],{"class":89},"file",[53,894,20],{"class":70},[53,896,897],{"class":66},"slice",[53,899,154],{"class":112},[53,901,863],{"class":89},[53,903,93],{"class":70},[53,905,849],{"class":89},[53,907,160],{"class":112},[53,909,93],{"class":70},[53,911,912],{"class":89}," session_public_key",[53,914,261],{"class":112},[53,916,917,920],{"class":55,"line":317},[53,918,919],{"class":70},"        }",[53,921,261],{"class":112},[53,923,924],{"class":55,"line":330},[53,925,271],{"emptyLinePlaceholder":270},[53,927,928],{"class":55,"line":336},[53,929,930],{"class":222},"        \u002F\u002F Envoyer tout de suite, puis laisser le ramasse-miettes libérer le blob\n",[53,932,933,935,938,940,943,946],{"class":55,"line":671},[53,934,805],{"class":59},[53,936,937],{"class":89}," formData",[53,939,102],{"class":70},[53,941,942],{"class":70}," new",[53,944,945],{"class":66}," FormData",[53,947,71],{"class":112},[53,949,951,954,956,959,961,963,966,968,970,972,974,976,979,981],{"class":55,"line":950},14,[53,952,953],{"class":89},"        formData",[53,955,20],{"class":70},[53,957,958],{"class":66},"append",[53,960,154],{"class":112},[53,962,595],{"class":70},[53,964,965],{"class":598},"upload_file",[53,967,595],{"class":70},[53,969,93],{"class":70},[53,971,808],{"class":89},[53,973,93],{"class":70},[53,975,615],{"class":70},[53,977,978],{"class":598},"chunk.age",[53,980,595],{"class":70},[53,982,261],{"class":112},[53,984,986,989,992,994,997,999,1001,1003,1005,1007,1010,1012,1015,1017,1020],{"class":55,"line":985},15,[53,987,988],{"class":105},"        await",[53,990,991],{"class":66}," doUpload",[53,993,154],{"class":112},[53,995,996],{"class":89},"file_info",[53,998,93],{"class":70},[53,1000,766],{"class":89},[53,1002,93],{"class":70},[53,1004,937],{"class":89},[53,1006,93],{"class":70},[53,1008,1009],{"class":89}," onChunkProgress",[53,1011,93],{"class":70},[53,1013,1014],{"class":89}," abortController",[53,1016,20],{"class":70},[53,1018,1019],{"class":89},"signal",[53,1021,261],{"class":112},[53,1023,1025,1028,1031,1033,1036,1038,1042,1044],{"class":55,"line":1024},16,[53,1026,1027],{"class":70},"    }",[53,1029,1030],{"class":112},")()",[53,1032,20],{"class":70},[53,1034,1035],{"class":66},"catch",[53,1037,154],{"class":112},[53,1039,1041],{"class":1040},"sHdIc","err",[53,1043,793],{"class":59},[53,1045,138],{"class":70},[53,1047,1049,1052,1054],{"class":55,"line":1048},17,[53,1050,1051],{"class":89},"        cancelled",[53,1053,102],{"class":70},[53,1055,1057],{"class":1056},"sfNiH"," true\n",[53,1059,1061,1064,1066,1069,1072],{"class":55,"line":1060},18,[53,1062,1063],{"class":89},"        abortController",[53,1065,20],{"class":70},[53,1067,1068],{"class":66},"abort",[53,1070,1071],{"class":112},"() ",[53,1073,1074],{"class":222},"\u002F\u002F coupe les requêtes encore en vol\n",[53,1076,1078,1081],{"class":55,"line":1077},19,[53,1079,1080],{"class":105},"        throw",[53,1082,1083],{"class":89}," err\n",[53,1085,1087,1089],{"class":55,"line":1086},20,[53,1088,1027],{"class":70},[53,1090,1091],{"class":112},"))\n",[53,1093,1095],{"class":55,"line":1094},21,[53,1096,186],{"class":70},[53,1098,1100],{"class":55,"line":1099},22,[53,1101,271],{"emptyLinePlaceholder":270},[53,1103,1105,1107,1109,1111,1113],{"class":55,"line":1104},23,[53,1106,587],{"class":105},[53,1108,533],{"class":532},[53,1110,20],{"class":70},[53,1112,538],{"class":66},[53,1114,1115],{"class":89},"(tasks)\n",[13,1117,1118],{},"Chiffrer chaque morceau à part a trois avantages. La mémoire reste bornée : quelques morceaux en vol, jamais le fichier\nentier. Les morceaux se chiffrent et partent en parallèle. Et un morceau qui échoue se renvoie seul, sans reprendre le\ntransfert depuis le début. Le prix à payer est un en-tête age par morceau, quelques kilo-octets sur 8 Mo, ce qui reste\nnégligeable.",[13,1120,1121],{},"Pourquoi 8 Mo, et pas 1 ou 100 ? À cause de la mémoire. Un morceau ne se chiffre pas en flux : il est lu entièrement,\nchiffré, puis gardé sous forme de blob jusqu'à ce que son envoi se termine. Pendant le chiffrement, un morceau coûte à\npeu près deux fois sa taille, en clair puis chiffré. Avec 8 chiffrements en parallèle, ça représente de l'ordre de\n128 Mo, ce qu'un ordinateur portable ordinaire encaisse sans broncher. Le serveur a la même contrainte : il garde\nchaque morceau reçu en tampon le temps de l'écrire dans le stockage objet, et ces tampons se multiplient par le nombre\nd'envois simultanés de tous les utilisateurs. Des morceaux plus gros feraient grimper la mémoire des deux côtés. Des\nmorceaux plus petits multiplieraient les requêtes et les en-têtes pour rien. 8 Mo est le compromis que nous avons\nretenu.",[13,1123,1124,1125,1128,1129,1132],{},"La gestion d'erreur compte autant que le reste. Si un morceau échoue définitivement, le drapeau ",[17,1126,1127],{},"cancelled"," empêche les\nsuivants de démarrer, et l'",[17,1130,1131],{},"AbortController"," interrompt ceux qui étaient déjà en route. Sans ça, un fichier de 3 Go\ndont le deuxième morceau échoue continuerait d'envoyer les centaines d'autres pour rien.",[25,1134,1136],{"id":1135},"hors-du-thread-principal","Hors du thread principal",[13,1138,1139,1140,1144],{},"Chiffrer 8 Mo n'est pas instantané, et un navigateur qui chiffre sur le thread principal ne répond plus : barre de\nprogression figée, clics ignorés. Le chiffrement tourne donc dans un Web Worker.\n",[33,1141,1143],{"url":1142},"https:\u002F\u002Fgithub.com\u002FGoogleChromeLabs\u002Fcomlink","Comlink"," évite d'écrire à la main le protocole de\nmessages entre la page et le worker. Côté worker, il suffit d'exposer l'objet :",[43,1146,1149],{"className":45,"code":1147,"filename":1148,"language":48,"meta":49,"style":49},"import * as Comlink from 'comlink'\nimport {cryptoCore} from '#shared\u002Futils\u002Fcrypto-core'\n\nComlink.expose(cryptoCore)\n","app\u002Fworkers\u002Fcrypto.worker.ts",[17,1150,1151,1175,1196,1200],{"__ignoreMap":49},[53,1152,1153,1156,1158,1161,1164,1167,1169,1172],{"class":55,"line":56},[53,1154,1155],{"class":105},"import",[53,1157,839],{"class":70},[53,1159,1160],{"class":105}," as",[53,1162,1163],{"class":89}," Comlink ",[53,1165,1166],{"class":105},"from",[53,1168,615],{"class":70},[53,1170,1171],{"class":598},"comlink",[53,1173,1174],{"class":70},"'\n",[53,1176,1177,1179,1181,1184,1186,1189,1191,1194],{"class":55,"line":74},[53,1178,1155],{"class":105},[53,1180,86],{"class":70},[53,1182,1183],{"class":89},"cryptoCore",[53,1185,99],{"class":70},[53,1187,1188],{"class":105}," from",[53,1190,615],{"class":70},[53,1192,1193],{"class":598},"#shared\u002Futils\u002Fcrypto-core",[53,1195,1174],{"class":70},[53,1197,1198],{"class":55,"line":80},[53,1199,271],{"emptyLinePlaceholder":270},[53,1201,1202,1204,1206,1209],{"class":55,"line":115},[53,1203,1143],{"class":89},[53,1205,20],{"class":70},[53,1207,1208],{"class":66},"expose",[53,1210,1211],{"class":89},"(cryptoCore)\n",[13,1213,1214],{},"Côté page, on appelle les fonctions du worker comme des fonctions asynchrones ordinaires. Un détail fait la différence :",[43,1216,1219],{"className":45,"code":1217,"filename":1218,"language":48,"meta":49,"style":49},"const encryptWithRecipient = async (input: Blob, recipient: string): Promise\u003CBlob> => {\n    const data = new Uint8Array(await input.arrayBuffer())\n    const encrypted = await proxy.encryptChunkWithRecipient(\n        Comlink.transfer(data, [data.buffer]),\n        recipient\n    )\n    return new Blob([encrypted])\n}\n","app\u002Fcomposables\u002Fuse-age-crypto.ts",[17,1220,1221,1269,1298,1319,1350,1355,1359,1375],{"__ignoreMap":49},[53,1222,1223,1225,1228,1230,1233,1235,1238,1240,1243,1245,1248,1250,1253,1256,1258,1260,1263,1265,1267],{"class":55,"line":56},[53,1224,276],{"class":59},[53,1226,1227],{"class":89}," encryptWithRecipient ",[53,1229,282],{"class":70},[53,1231,1232],{"class":59}," async",[53,1234,740],{"class":70},[53,1236,1237],{"class":1040},"input",[53,1239,147],{"class":70},[53,1241,1242],{"class":532}," Blob",[53,1244,93],{"class":70},[53,1246,1247],{"class":1040}," recipient",[53,1249,147],{"class":70},[53,1251,1252],{"class":532}," string",[53,1254,1255],{"class":70},"):",[53,1257,533],{"class":532},[53,1259,758],{"class":70},[53,1261,1262],{"class":532},"Blob",[53,1264,364],{"class":70},[53,1266,793],{"class":59},[53,1268,138],{"class":70},[53,1270,1271,1273,1276,1278,1280,1283,1285,1287,1290,1292,1295],{"class":55,"line":74},[53,1272,83],{"class":59},[53,1274,1275],{"class":89}," data",[53,1277,102],{"class":70},[53,1279,942],{"class":70},[53,1281,1282],{"class":66}," Uint8Array",[53,1284,154],{"class":112},[53,1286,587],{"class":105},[53,1288,1289],{"class":89}," input",[53,1291,20],{"class":70},[53,1293,1294],{"class":66},"arrayBuffer",[53,1296,1297],{"class":112},"())\n",[53,1299,1300,1302,1305,1307,1309,1312,1314,1317],{"class":55,"line":80},[53,1301,83],{"class":59},[53,1303,1304],{"class":89}," encrypted",[53,1306,102],{"class":70},[53,1308,106],{"class":105},[53,1310,1311],{"class":89}," proxy",[53,1313,20],{"class":70},[53,1315,1316],{"class":66},"encryptChunkWithRecipient",[53,1318,314],{"class":112},[53,1320,1321,1324,1326,1329,1331,1334,1336,1338,1340,1342,1345,1348],{"class":55,"line":115},[53,1322,1323],{"class":89},"        Comlink",[53,1325,20],{"class":70},[53,1327,1328],{"class":66},"transfer",[53,1330,154],{"class":112},[53,1332,1333],{"class":89},"data",[53,1335,93],{"class":70},[53,1337,514],{"class":112},[53,1339,1333],{"class":89},[53,1341,20],{"class":70},[53,1343,1344],{"class":89},"buffer",[53,1346,1347],{"class":112},"])",[53,1349,163],{"class":70},[53,1351,1352],{"class":55,"line":132},[53,1353,1354],{"class":89},"        recipient\n",[53,1356,1357],{"class":55,"line":141},[53,1358,447],{"class":112},[53,1360,1361,1363,1365,1367,1370,1373],{"class":55,"line":166},[53,1362,135],{"class":105},[53,1364,942],{"class":70},[53,1366,1242],{"class":66},[53,1368,1369],{"class":112},"([",[53,1371,1372],{"class":89},"encrypted",[53,1374,578],{"class":112},[53,1376,1377],{"class":55,"line":177},[53,1378,186],{"class":70},[13,1380,1381,1382,1385,1386,1389,1390,1394],{},"Par défaut, ",[17,1383,1384],{},"postMessage"," copie les données qu'il envoie au worker. ",[17,1387,1388],{},"Comlink.transfer()"," déclare le buffer comme\n",[1391,1392,1393],"em",{},"transférable"," : sa propriété passe au worker sans copie, et la page ne peut plus y toucher. Sur un fichier de 3 Go,\nc'est près de 400 copies de 8 Mo en moins.",[25,1396,1398],{"id":1397},"le-réseau","Le réseau",[13,1400,1401],{},"Envoyer plusieurs morceaux en parallèle accélère l'upload sur une bonne connexion. Sur une connexion lente, c'est\nl'inverse. Les envois se partagent la bande passante, et chaque morceau met d'autant plus longtemps à partir. Or chaque\nenvoi a un délai maximal : passé 119 secondes, la requête est abandonnée.",[13,1403,1404],{},"Prenons une connexion à 1 Mbit\u002Fs en envoi ce qu'on trouve encore sur de l'ADSL ou une 4G\nsaturée. Un morceau de 8 Mo seul passe en un peu plus d'une minute. Huit morceaux en parallèle se partagent le même\ndébit, et chacun mettrait plus de huit minutes : ils échoueraient tous au bout de deux minutes, et l'upload ne finirait\njamais. Sur une fibre, au contraire, un seul morceau à la fois laisserait la plus grande partie du débit inutilisée.",[13,1406,1407,1408,1411],{},"Il n'existe donc pas de bon réglage fixe. Retyc s'adapte à la connexion. Le chiffrement passe par ",[17,1409,1410],{},"p-limit",", 8 morceaux\nà la fois au plus. L'envoi passe par un sémaphore adaptatif, qui commence prudemment avec un seul morceau en vol.\nÀ chaque morceau terminé, il mesure le débit, le lisse avec une moyenne mobile pondérée (EWMA), et en déduit combien de\nmorceaux envoyer en parallèle pour que chacun mette environ 5 secondes, très loin du délai maximal :",[43,1413,1415],{"className":45,"code":1414,"filename":693,"language":48,"meta":49,"style":49},"const speed = (estimatedByteSize \u002F durationSeconds) * concurrencySnapshot\newmaSpeed = ewmaSpeed === null\n    ? speed\n    : ADAPTIVE_EWMA_ALPHA * speed + (1 - ADAPTIVE_EWMA_ALPHA) * ewmaSpeed \u002F\u002F alpha = 0.3\n\nconst rawTarget = (ewmaSpeed * ADAPTIVE_TARGET_SECONDS) \u002F chunkSize \u002F\u002F cible : 5 s par morceau\n\u002F\u002F +2 au plus par mesure, pour ne pas noyer une connexion lente\nsemaphore.setTarget(Math.min(rawTarget, semaphore.target + ADAPTIVE_RAMP_UP_STEP))\n",[17,1416,1417,1440,1456,1464,1497,1501,1526,1531],{"__ignoreMap":49},[53,1418,1419,1421,1424,1426,1429,1431,1434,1437],{"class":55,"line":56},[53,1420,276],{"class":59},[53,1422,1423],{"class":89}," speed ",[53,1425,282],{"class":70},[53,1427,1428],{"class":89}," (estimatedByteSize ",[53,1430,722],{"class":70},[53,1432,1433],{"class":89}," durationSeconds) ",[53,1435,1436],{"class":70},"*",[53,1438,1439],{"class":89}," concurrencySnapshot\n",[53,1441,1442,1445,1447,1450,1453],{"class":55,"line":74},[53,1443,1444],{"class":89},"ewmaSpeed ",[53,1446,282],{"class":70},[53,1448,1449],{"class":89}," ewmaSpeed ",[53,1451,1452],{"class":70},"===",[53,1454,1455],{"class":70}," null\n",[53,1457,1458,1461],{"class":55,"line":80},[53,1459,1460],{"class":70},"    ?",[53,1462,1463],{"class":89}," speed\n",[53,1465,1466,1469,1472,1474,1476,1479,1481,1484,1487,1490,1492,1494],{"class":55,"line":115},[53,1467,1468],{"class":70},"    :",[53,1470,1471],{"class":89}," ADAPTIVE_EWMA_ALPHA ",[53,1473,1436],{"class":70},[53,1475,1423],{"class":89},[53,1477,1478],{"class":70},"+",[53,1480,740],{"class":89},[53,1482,1483],{"class":367},"1",[53,1485,1486],{"class":70}," -",[53,1488,1489],{"class":89}," ADAPTIVE_EWMA_ALPHA) ",[53,1491,1436],{"class":70},[53,1493,1449],{"class":89},[53,1495,1496],{"class":222},"\u002F\u002F alpha = 0.3\n",[53,1498,1499],{"class":55,"line":132},[53,1500,271],{"emptyLinePlaceholder":270},[53,1502,1503,1505,1508,1510,1513,1515,1518,1520,1523],{"class":55,"line":141},[53,1504,276],{"class":59},[53,1506,1507],{"class":89}," rawTarget ",[53,1509,282],{"class":70},[53,1511,1512],{"class":89}," (ewmaSpeed ",[53,1514,1436],{"class":70},[53,1516,1517],{"class":89}," ADAPTIVE_TARGET_SECONDS) ",[53,1519,722],{"class":70},[53,1521,1522],{"class":89}," chunkSize ",[53,1524,1525],{"class":222},"\u002F\u002F cible : 5 s par morceau\n",[53,1527,1528],{"class":55,"line":166},[53,1529,1530],{"class":222},"\u002F\u002F +2 au plus par mesure, pour ne pas noyer une connexion lente\n",[53,1532,1533,1536,1538,1541,1544,1546,1548,1551,1553,1556,1558,1561,1563],{"class":55,"line":177},[53,1534,1535],{"class":89},"semaphore",[53,1537,20],{"class":70},[53,1539,1540],{"class":66},"setTarget",[53,1542,1543],{"class":89},"(Math",[53,1545,20],{"class":70},[53,1547,858],{"class":66},[53,1549,1550],{"class":89},"(rawTarget",[53,1552,93],{"class":70},[53,1554,1555],{"class":89}," semaphore",[53,1557,20],{"class":70},[53,1559,1560],{"class":89},"target ",[53,1562,1478],{"class":70},[53,1564,1565],{"class":89}," ADAPTIVE_RAMP_UP_STEP))\n",[13,1567,1568],{},"La montée est volontairement lente, deux morceaux de plus par mesure au maximum, et la cible est plafonnée à 8. Une\nfibre atteint vite le plafond, une connexion lente reste à un seul morceau en vol, et personne n'a rien à régler. Le\nplafond protège aussi la mémoire du serveur : même sur la meilleure connexion, un upload n'occupe jamais plus de\n8 tampons de réception à la fois. L'objectif est d'aller aussi vite que la connexion le permet, sans jamais\néchouer sur une connexion lente, et sans dépenser plus de mémoire que nécessaire, ni dans le navigateur ni sur nos\nserveurs.",[25,1570,1572],{"id":1571},"côté-serveur-ce-que-lapi-accepte-et-ce-quelle-refuse","Côté serveur : ce que l'API accepte et ce qu'elle refuse",[13,1574,1575],{},"L'API ne déchiffre rien, puisqu'elle n'a aucune clé. Elle vérifie les quotas et écrit chaque morceau tel quel dans le\nstockage objet. Elle a pourtant un rôle à jouer : refuser tout ce qui affaiblirait le schéma. Chaque clé publique qui\nentre dans le système passe par ce type Pydantic :",[43,1577,1582],{"className":1578,"code":1579,"filename":1580,"language":1581,"meta":49,"style":49},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","class AgePublicKey(str):\n    \"\"\"Age public key, hybrid post-quantum recipients only (`age1pq1...`).\n\n    Classic X25519 recipients (`age1...`) are valid age keys but are refused\n    everywhere: every identity in the product is generated as a hybrid\n    post-quantum pair, and a classic key slipping in would silently weaken\n    the post-quantum guarantee of whatever it protects.\n    \"\"\"\n    REGEX_PATTERN = r\"^age1pq1[0-9a-z]{1000,2500}$\"\n","app\u002Fmodel\u002Ftypes\u002Fage_public_key.py","python",[17,1583,1584,1589,1594,1598,1603,1608,1613,1618,1623],{"__ignoreMap":49},[53,1585,1586],{"class":55,"line":56},[53,1587,1588],{},"class AgePublicKey(str):\n",[53,1590,1591],{"class":55,"line":74},[53,1592,1593],{},"    \"\"\"Age public key, hybrid post-quantum recipients only (`age1pq1...`).\n",[53,1595,1596],{"class":55,"line":80},[53,1597,271],{"emptyLinePlaceholder":270},[53,1599,1600],{"class":55,"line":115},[53,1601,1602],{},"    Classic X25519 recipients (`age1...`) are valid age keys but are refused\n",[53,1604,1605],{"class":55,"line":132},[53,1606,1607],{},"    everywhere: every identity in the product is generated as a hybrid\n",[53,1609,1610],{"class":55,"line":141},[53,1611,1612],{},"    post-quantum pair, and a classic key slipping in would silently weaken\n",[53,1614,1615],{"class":55,"line":166},[53,1616,1617],{},"    the post-quantum guarantee of whatever it protects.\n",[53,1619,1620],{"class":55,"line":177},[53,1621,1622],{},"    \"\"\"\n",[53,1624,1625],{"class":55,"line":183},[53,1626,1627],{},"    REGEX_PATTERN = r\"^age1pq1[0-9a-z]{1000,2500}$\"\n",[13,1629,1630,1631,1634,1635,1637],{},"Le commentaire dit l'essentiel. Une clé age classique est parfaitement valide. age lui-même refuse d'ailleurs de\nmélanger une clé classique et une clé post-quantique dans un même fichier, grâce à un label ",[17,1632,1633],{},"postquantum"," porté par les\nclés hybrides. Mais il suffirait qu'une clé classique entre dans le système, par exemple comme clé d'un utilisateur,\npour que tout ce qu'on chiffre pour elle ne soit protégé que par X25519, et devienne lisible le jour où un ordinateur\nquantique saura le casser. La règle est donc posée à l'entrée : une clé qui ne commence pas par ",[17,1636,200],{}," est refusée\ndès la validation de la requête, avant d'atteindre le moindre service.",[25,1639,1641],{"id":1640},"ce-que-nous-voyons-quand-même","Ce que nous voyons quand même",[13,1643,1644],{},"Chiffrer de bout en bout ne rend pas tout invisible, et mieux vaut le dire. Voici ce qui arrive chez nous, et sous\nquelle forme :",[1646,1647,1648,1661],"table",{},[1649,1650,1651],"thead",{},[1652,1653,1654,1658],"tr",{},[1655,1656,1657],"th",{},"Donnée",[1655,1659,1660],{},"Ce que le serveur reçoit",[1662,1663,1664,1673,1681,1688,1696,1704,1711,1719],"tbody",{},[1652,1665,1666,1670],{},[1667,1668,1669],"td",{},"Contenu des fichiers",[1667,1671,1672],{},"Chiffré",[1652,1674,1675,1678],{},[1667,1676,1677],{},"Noms, chemins et types des fichiers",[1667,1679,1680],{},"Chiffrés",[1652,1682,1683,1686],{},[1667,1684,1685],{},"Message accompagnant le transfert",[1667,1687,1672],{},[1652,1689,1690,1693],{},[1667,1691,1692],{},"Clé de session",[1667,1694,1695],{},"Chiffrée pour chaque destinataire",[1652,1697,1698,1701],{},[1667,1699,1700],{},"Taille des fichiers, nombre de morceaux",[1667,1702,1703],{},"En clair",[1652,1705,1706,1709],{},[1667,1707,1708],{},"Titre du transfert",[1667,1710,1703],{},[1652,1712,1713,1716],{},[1667,1714,1715],{},"Adresses e-mail des destinataires",[1667,1717,1718],{},"En clair, pour leur envoyer la notification",[1652,1720,1721,1724],{},[1667,1722,1723],{},"Compte de l'expéditeur, dates, adresses IP",[1667,1725,1703],{},[13,1727,1728],{},"Le titre et les adresses restent lisibles parce que le service en a besoin pour fonctionner : afficher la liste de vos\ntransferts, prévenir vos destinataires par e-mail. Si le titre vous paraît sensible, laissez-le vide, ou mettez\nl'information dans le message, qui est chiffré.",[25,1730,1732],{"id":1731},"bonus-changer-la-serrure-sans-tout-rechiffrer","Bonus : changer la serrure sans tout rechiffrer",[13,1734,1735],{},"La clé de session a un dernier avantage, moins visible. Quand un utilisateur fait tourner sa clé personnelle, ses\nanciens transferts doivent devenir lisibles avec la nouvelle. Rechiffrer des centaines de gigaoctets serait long et\ncoûteux. Il suffit de rechiffrer l'enveloppe : le navigateur ouvre la clé de session avec l'ancienne clé, la chiffre\navec la nouvelle, et envoie le résultat.",[43,1737,1740],{"className":1578,"code":1738,"filename":1739,"language":1581,"meta":49,"style":49},"@router.put(\"\u002Fshare\u002F{share_id}\u002Frekey\", status_code=status.HTTP_204_NO_CONTENT, operation_id=\"rekeyShare\")\ndef rekey_share(share: SharePrivateDepend, data: ShareRekeyRequest, session: SessionDep, logger: LoggerDep):\n    svc_rekey_share(share, data.session_private_key_enc, session, logger)\n","app\u002Fapi\u002Fprivate\u002Fshare\u002Frouter.py",[17,1741,1742,1747,1752],{"__ignoreMap":49},[53,1743,1744],{"class":55,"line":56},[53,1745,1746],{},"@router.put(\"\u002Fshare\u002F{share_id}\u002Frekey\", status_code=status.HTTP_204_NO_CONTENT, operation_id=\"rekeyShare\")\n",[53,1748,1749],{"class":55,"line":74},[53,1750,1751],{},"def rekey_share(share: SharePrivateDepend, data: ShareRekeyRequest, session: SessionDep, logger: LoggerDep):\n",[53,1753,1754],{"class":55,"line":80},[53,1755,1756],{},"    svc_rekey_share(share, data.session_private_key_enc, session, logger)\n",[13,1758,1759],{},"La requête ne transporte que quelques kilo-octets, même pour un transfert de 3 Go. Les morceaux stockés ne bougent\npas.",[25,1761,1763],{"id":1762},"pour-vérifier-par-vous-même","Pour vérifier par vous-même",[13,1765,1766,1767,1770,1771,1775],{},"Vous n'avez pas à nous croire sur parole pour tout ça.\nLa ",[33,1768,1769],{"url":35},"spécification d'age"," est publique.\nNotre ",[33,1772,1774],{"url":1773},"https:\u002F\u002Fgithub.com\u002Fretyc\u002Fretyc-cli","CLI",", publié sous licence MIT, applique le\nmême schéma en Go : clé de session hybride, enveloppes par destinataire, morceaux chiffrés un par un. Vous pouvez lire\nson code, ou envoyer un transfert avec et observer ce qui part sur le réseau.",[13,1777,1778,1779,1784,1785,20],{},"Pour une description complète de l'architecture et de ses limites, le ",[1780,1781,1783],"a",{"href":1782},"\u002Ffr\u002Fresources\u002Fwhite-paper","livre blanc"," est en\naccès libre. Et si la différence entre « chiffré » et « chiffré de bout en bout » vous semble floue, nous l'avons\nexpliquée dans ",[1780,1786,1788],{"href":1787},"\u002Ffr\u002Fblog\u002Fencrypted-does-not-mean-end-to-end","un article dédié",[1790,1791,1792],"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 .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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 .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 .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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}",{"title":49,"searchDepth":74,"depth":74,"links":1794},[1795,1796,1797,1798,1799,1800,1801,1802,1803,1804],{"id":27,"depth":74,"text":28},{"id":207,"depth":74,"text":208},{"id":457,"depth":74,"text":458},{"id":681,"depth":74,"text":682},{"id":1135,"depth":74,"text":1136},{"id":1397,"depth":74,"text":1398},{"id":1571,"depth":74,"text":1572},{"id":1640,"depth":74,"text":1641},{"id":1731,"depth":74,"text":1732},{"id":1762,"depth":74,"text":1763},"Tech","2026-09-25","Une paire de clés post-quantique par transfert, des morceaux de 8 Mo chiffrés un par un dans un Web Worker, une API qui refuse les clés classiques. Ce que fait le navigateur entre le moment où vous lâchez un fichier et celui où le dernier octet part, code à l'appui.","md",null,{},"\u002Fblog\u002Ffr\u002Fanatomy-of-an-encrypted-upload",{"title":1813,"description":1814,"ogTitle":5,"ogDescription":1815},"Chiffrement de bout en bout dans le navigateur : anatomie d'un upload (age, ML-KEM, Web Worker)","Comment Retyc chiffre un fichier dans le navigateur avant l'envoi : clé de session hybride ML-KEM-768 + X25519, chiffrement d'enveloppe, découpage en morceaux, Web Worker et Comlink, validation côté API. Avec le code.","Clés post-quantiques, morceaux de 8 Mo, Web Worker : ce qui se passe avant que le premier octet quitte votre machine.",{"loc":1811},"blog\u002Ffr\u002Fanatomy-of-an-encrypted-upload",[1819,36,1820,1821,1822],"chiffrement de bout en bout","post-quantique","Web Worker","TypeScript","rR6stdkR68weIAtLr2zO4Ukmdhu7wXd-jLJIYL8tYTw",[1809,1825],{"title":1826,"path":1827,"stem":1828,"description":1829,"children":-1},"Envoyer un fichier par un service de transfert gratuit : ce que vous confiez vraiment","\u002Fblog\u002Ffr\u002Ffile-transfer-without-end-to-end-encryption","blog\u002Ffr\u002Ffile-transfer-without-end-to-end-encryption","Glisser un fichier, saisir une adresse, cliquer sur « Envoyer ». Les services de transfert de fichiers grand public sont devenus un réflexe en entreprise. Sans chiffrement de bout en bout, ils voient tout ce qui passe. Cinq situations professionnelles pour comprendre ce que cela implique."]