Repository navigation
fix(fenrir): support read-token rotation and encrypted chapter decryption - #2773
Conversation
[Medium impact] The changes stay within Fenrir Realm chapter reading. Fix the shared-token race before merging; overlapping chapter loads can fail. FindingsSummaryUpdates Fenrir Realm to version
Reviews (1) · Last reviewed commit: "fix(fenrir): support read-token rotation..." · Reviewed by Greptile |
| const token = await this.getReadToken(json.id); | ||
| let decrypted = token ? this.decryptWithToken(json, token) : null; |
There was a problem hiding this comment.
Chapter loads overwrite needed tokens
parseChapter fetches encrypted content with one token, then reads the token again from the shared cachedReadToken. If another chapter load rotates that token while the request is waiting, the first load decrypts with the wrong key. The retry uses the same shared state, so overlapping loads can exhaust the retry and report a Turnstile error for a readable chapter.
Keep the token used by each request with its response, and use that token for decryption.
| function unshuffleFragments(fragments: string[], seedStr: string): string { | ||
| const count = fragments.length; | ||
| if (count < 2) return count === 1 ? fragments[0] : ''; | ||
| const digest = sha256Bytes(seedStr); |
There was a problem hiding this comment.
The new handwritten hash, fragment shuffle, and decryption code has no automated tests in this PR. Add fixed input/output examples for both fragmented-v1 and fragmented-v2, including token rotation. A byte-order or shuffle-direction mistake otherwise appears only as a failed chapter read, and the current error points users to Turnstile rather than the decoding failure.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Description
Fixes empty/failing chapters on Fenrir Realm (
fenrir) caused by their new chapter-boundfr_readread-token verification andfragmented-v2/fragmented-v1AES-256-GCM content encryption.Previously:
/api/new/v2/chapters/{id}returnedcontent_format: "locked"with an emptycontentstring after reading 1–2 chapters becausefr_readtokens are bound to a single chapter ID (cid) and must be rotated viaPOST /api/read-token({ "chapter_id": "..." }) when moving between chapters.X-Accepts-Encrypted-Content: 3is supplied, chapter content is returned as 64 Base64 fragments shuffled with a SHA-256-seeded Xorshift128 Fisher–Yates permutation (fr-frag-v2/fr-frag-v1) and encrypted with AES-256-GCM (fr-content-v2/fr-content-v1).fr_vidcookie (00000000-0000-4000-80a5-a5a5a59e3779) that triggers global429 Too Many Attempts(leg: vid) rate-limiting on/api/new/v2/chapters/{id}unlessCookie: fr_read=<token>is scoped cleanly.Changes
POST /api/read-tokenandGET /api/read-tokentoken rotation & caching (mintReadToken,getReadToken,parseReadTokenPayload,captureReadTokenHeader) with automatic fallback to the native WebView CookieJar when an in-memory token expires or is invalidated.fragmented-v2andfragmented-v1unshuffling (xorshift128Next,unshuffleFragments) and AES-256-GCM decryption (decryptWithTokenusing@libs/aesgcmand inlinesha256Bytesfor Hermes/React Native compatibility).[aria-hidden="true"],.reader-attribution,[data-fr-attr],style, script, copy/bookmark watermarks, and zero-width characters[\u200B-\u200D\uFEFF]).parseNovelchapter path construction to usecleanNovelPath.fenrirplugin version from1.1.3to1.1.4.Closes #2704
Closes #2707
Closes #2769
Checklist
type(scope): description(e.g.feat(<generator>): add new source)