Changeset 58cad21 in Klonkt for src/services/OpenWebAuthService.js
- Timestamp:
- 08/19/2026 07:44:50 PM (3 weeks ago)
- Branches:
- main
- Children:
- 598f090
- Parents:
- 5869b1f
- git-author:
- Robin <roboburr@…> (08/19/2026 07:30:37 PM)
- git-committer:
- Robin <roboburr@…> (08/19/2026 07:44:50 PM)
- File:
-
- 1 edited
-
src/services/OpenWebAuthService.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/OpenWebAuthService.js
r5869b1f r58cad21 233 233 } 234 234 235 /** 236 * Een deterministische nep-uitkomst, afgeleid uit de ciphertext en onze eigen 237 * sleutel. Dit is de kern van implicit rejection: bij ongeldige padding geven we 238 * GEEN fout maar een waarde, zodat "klopte de padding" nergens af te lezen is. 239 * 240 * DETERMINISTISCH, en dat is geen detail. Zou dit verse willekeur zijn, dan 241 * geeft dezelfde ciphertext twee keer aanbieden twee verschillende antwoorden -- 242 * en juist dat verschil is het onderscheid dat we wilden verbergen. Zo doen TLS 243 * en OpenSSL 3.2 het ook: afgeleid uit sleutel + ciphertext, dus stabiel bij 244 * herhaling en onvoorspelbaar voor wie de sleutel niet heeft. 245 * 246 * Geëxporteerd omdat die eigenschap toetsbaar moet zijn; buiten de tests heeft 247 * niemand hem nodig. 248 * 249 * EERLIJK OVER WAT DIT WEL EN NIET DRAAGT (gemeten 19-8): haal je hem weg, dan 250 * blijft de suite groen. De andere tak geeft dan een LEGE string terug, en die 251 * sneuvelt net zo goed op de tekenset-controle hieronder -- "werpt niet" en 252 * "levert geen token" zijn dus al gedekt zonder deze functie. Wat hij toevoegt 253 * is dat ALLE faalwegen dezelfde vorm teruggeven: verkeerde sleutel, verkeerde 254 * lengte, kapotte base64, ongeldige padding. Een lege string is een verklikker 255 * voor wie ooit naar de rauwe waarde kijkt in plaats van naar het eindoordeel; 256 * afgeleide bytes zijn dat niet. Zo doen TLS en OpenSSL 3.2 het ook. 257 */ 258 export function _nepUitkomst(privatePem, ct) { 259 const geheim = crypto.createHash('sha256').update(String(privatePem)).digest(); 260 return crypto.createHmac('sha256', geheim).update(ct).digest().toString('latin1'); 261 } 262 263 /** 264 * PKCS#1 v1.5 zelf uitpakken (EME-PKCS1-v1_5: 00 02 PS 00 M). 265 * 266 * WAAROM ZELF: Node weigert `privateDecrypt` met RSA_PKCS1_PADDING sinds de 267 * mitigatie voor CVE-2023-46809 (Marvin). De revert-vlag bestaat alleen op de 268 * lijnen 18/20/21 -- Node 22+ heeft hem nooit gehad, en 20 is sinds 30 april 269 * 2026 EOL. Er is dus geen weg terug; zie shaer-r15. 270 * 271 * OpenWebAuth (FEP-61cf) schrijft v1.5 voor, dus overstappen op OAEP repareert 272 * de fout en breekt de interop met Hubzilla. Blijft over: `RSA_NO_PADDING` en 273 * het omhulsel er zelf afhalen -- precies het stuk dat de CVE veroorzaakte, dus 274 * met de zorg die daarbij hoort. 275 * 276 * GEEN VROEGE UITGANG EN GEEN WORP. De scan loopt altijd het hele blok af en 277 * beide takken doen hetzelfde werk. Dat is geen echte constant-time -- die 278 * krijg je in JavaScript met JIT en GC niet -- maar het haalt wel het 279 * waarneembare verschil weg. Wat de aanval hier echt begrenst is de teller op 280 * /magic: een orakel heeft honderdduizenden pogingen nodig. 281 */ 282 function pakUit(blok, privatePem, ct) { 283 const k = blok.length; 284 // Kop: 00 02. Als getal uitrekenen, niet als vertakking. 285 let goed = ((blok[0] === 0x00) & (blok[1] === 0x02)); 286 // Eerste nulbyte vanaf 2 zoeken ZONDER de lus te verlaten. 287 let sep = -1; 288 for (let i = 2; i < k; i++) { 289 const isNul = blok[i] === 0x00 ? 1 : 0; 290 const nogNiet = sep === -1 ? 1 : 0; 291 sep = sep + (isNul & nogNiet) * (i - sep); 292 } 293 // PS moet minstens 8 bytes zijn (RFC 8017), dus de scheider ligt op >= 10. 294 goed = goed & (sep >= 10 ? 1 : 0) & (sep < k ? 1 : 0); 295 const echt = blok.subarray(goed ? sep + 1 : k).toString('utf8'); 296 const nep = _nepUitkomst(privatePem, ct); 297 return goed ? echt : nep; 298 } 299 235 300 /** Het token uitpakken met onze eigen prive-sleutel. */ 236 301 export function decryptToken(encrypted, privatePem) { 237 302 const b64 = String(encrypted || '').replace(/-/g, '+').replace(/_/g, '/'); 238 const buf = crypto.privateDecrypt( 239 { key: privatePem, padding: crypto.constants.RSA_PKCS1_PADDING }, 240 Buffer.from(b64, 'base64'), 241 ); 242 const t = buf.toString('utf8'); 243 // Een token is URL-veilige tekst. Bij een verkeerde sleutel geeft PKCS#1 v1.5 244 // geen fout maar afgeleide onzin terug (implicit rejection, zie de toelichting 245 // bij encryptTokenFor), en die onzin hoort hier te stranden in plaats van als 246 // token de wereld in te gaan. 303 const ct = Buffer.from(b64, 'base64'); 304 let k = 0; 305 try { k = crypto.createPublicKey(privatePem).asymmetricKeyDetails.modulusLength / 8; } catch { k = 0; } 306 307 // Een blok van de verkeerde lengte zegt niets over de sleutel, maar het zou 308 // wel werpen -- en een worp is precies het signaal dat we kwijt willen. Dus 309 // dezelfde weg als een ongeldige padding. 310 let blok = null; 311 if (k && ct.length === k) { 312 try { 313 blok = crypto.privateDecrypt({ key: privatePem, padding: crypto.constants.RSA_NO_PADDING }, ct); 314 } catch { blok = null; } 315 } 316 const t = (blok && blok.length === k) ? pakUit(blok, privatePem, ct) : _nepUitkomst(privatePem, ct); 317 318 // Een token is URL-veilige tekst. Wat hierboven uit een mislukking komt is 319 // afgeleide onzin, en die hoort hier te stranden in plaats van als token de 320 // wereld in te gaan. 247 321 // 248 322 // ALLEBEI de voorwaarden doen werk, en dat is gemeten met 300 vreemde sleutels: 249 323 // - de TEKENSET vangt vrijwel alles. Van die 300 was er geen enkele die 250 324 // volledig uit URL-veilige tekens bestond. 251 // - de ONDERGRENS vangt de rest. De onzin heeft een willekeurige lengte (5 252 // tot 209 bytes gezien), en 18 van de 300 was korter dan 16 bytes. Bij zo'n 253 // kort stukje is "toevallig allemaal URL-veilig" niet meer verwaarloosbaar: 254 // per byte is die kans ruwweg een kwart. 325 // - de ONDERGRENS vangt de rest. De onzin heeft een willekeurige lengte, en 326 // bij een kort stukje is "toevallig allemaal URL-veilig" niet meer 327 // verwaarloosbaar: per byte is die kans ruwweg een kwart. 255 328 // Zestien is daarmee geen rond getal maar een grens die iets doet. Een echte 256 329 // implementatie zit er ruim boven (de onze: 43 tekens).
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)