Skip to content

fix(fenrir): support read-token rotation and encrypted chapter decryption - #2773

Merged
rajarsheechatterjee merged 1 commit into
lnreader:masterfrom
7ui77:fix/fenrir-encrypted-chapters
Oct 25, 2026
Merged

rajarsheechatterjee merged 1 commit into
lnreader:masterfrom
7ui77:fix/fenrir-encrypted-chapters

Conversation

@7ui77

@7ui77 7ui77 commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes empty/failing chapters on Fenrir Realm (fenrir) caused by their new chapter-bound fr_read read-token verification and fragmented-v2 / fragmented-v1 AES-256-GCM content encryption.

Previously:

  • /api/new/v2/chapters/{id} returned content_format: "locked" with an empty content string after reading 1–2 chapters because fr_read tokens are bound to a single chapter ID (cid) and must be rotated via POST /api/read-token ({ "chapter_id": "..." }) when moving between chapters.
  • When X-Accepts-Encrypted-Content: 3 is 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).
  • Fenrir Realm's analytics endpoint sets a shared anonymous fr_vid cookie (00000000-0000-4000-80a5-a5a5a59e3779) that triggers global 429 Too Many Attempts (leg: vid) rate-limiting on /api/new/v2/chapters/{id} unless Cookie: fr_read=<token> is scoped cleanly.

Changes

  • Implement POST /api/read-token and GET /api/read-token token rotation & caching (mintReadToken, getReadToken, parseReadTokenPayload, captureReadTokenHeader) with automatic fallback to the native WebView CookieJar when an in-memory token expires or is invalidated.
  • Implement fragmented-v2 and fragmented-v1 unshuffling (xorshift128Next, unshuffleFragments) and AES-256-GCM decryption (decryptWithToken using @libs/aes gcm and inline sha256Bytes for Hermes/React Native compatibility).
  • Strip anti-scraping honeypot elements ([aria-hidden="true"], .reader-attribution, [data-fr-attr], style, script, copy/bookmark watermarks, and zero-width characters [\u200B-\u200D\uFEFF]).
  • Fix parseNovel chapter path construction to use cleanNovelPath.
  • Bump fenrir plugin version from 1.1.3 to 1.1.4.

Closes #2704
Closes #2707
Closes #2769

Checklist

  • Update version code if an existing plugin was modified
  • Test changes in Plugin Playground or the app
  • Reference related issues in the PR body (e.g. Closes #xyz)
  • Commit messages follow type(scope): description (e.g. feat(<generator>): add new source)

@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes stay within Fenrir Realm chapter reading.

Fix the shared-token race before merging; overlapping chapter loads can fail.

Findings

  1. P1 Chapter loads overwrite needed tokens ▶
  2. P2 New decoding code lacks tests ▶

Summary

Updates Fenrir Realm to version 1.1.4.

  • Fenrir chapters get fresh read tokens and open encrypted content.
  • Chapter text loses hidden marks and keeps one formatting path.

Reviews (1) · Last reviewed commit: "fix(fenrir): support read-token rotation..." · Reviewed by Greptile

Comment on lines +803 to +804
const token = await this.getReadToken(json.id);
let decrypted = token ? this.decryptWithToken(json, token) : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +130 to +133
function unshuffleFragments(fragments: string[], seedStr: string): string {
const count = fragments.length;
if (count < 2) return count === 1 ? fragments[0] : '';
const digest = sha256Bytes(seedStr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New decoding code lacks tests

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!

@rajarsheechatterjee
rajarsheechatterjee merged commit 577fa9c into lnreader:master Oct 25, 2026
3 of 4 checks passed
@7ui77
7ui77 deleted the fix/fenrir-encrypted-chapters branch October 25, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fenrir] Empty chapter: After the Villain Awakens, the Heroines All Turned to Me — Chapter 1 [fenrir] Empty chapter: Fenrir crash

2 participants