MangaFire: fix mobile useragent requirment & trust all certs (#11065)

* MangaFire: load webResource via okhttp

removes mobile chrome ua dependency

* trust all certs

* convert vrf script to kotlin

taken from: KotatsuApp/kotatsu-parsers

* bump
This commit is contained in:
AwkwardPeak7 2025-10-15 17:46:56 +05:00 committed by Draff
parent 979ae7f53f
commit 145dc251e6
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 247 additions and 320 deletions

View File

@ -1,294 +0,0 @@
/**
Copyright © 2019 W3C and Jeff Carpenter <jeffcarp@chromium.org>
atob and btoa source code is released under the 3-Clause BSD license.
*/
function atob(data) {
if (arguments.length === 0) {
throw new TypeError("1 argument required, but only 0 present.");
}
const keystr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function atobLookup(chr) {
const index = keystr.indexOf(chr);
// Throw exception if character is not in the lookup string; should not be hit in tests
return index < 0 ? undefined : index;
}
data = `${data}`;
data = data.replace(/[ \t\n\f\r]/g, "");
if (data.length % 4 === 0) {
data = data.replace(/==?$/, "");
}
if (data.length % 4 === 1 || /[^+/0-9A-Za-z]/.test(data)) {
return null;
}
let output = "";
let buffer = 0;
let accumulatedBits = 0;
for (let i = 0; i < data.length; i++) {
buffer <<= 6;
buffer |= atobLookup(data[i]);
accumulatedBits += 6;
if (accumulatedBits === 24) {
output += String.fromCharCode((buffer & 0xff0000) >> 16);
output += String.fromCharCode((buffer & 0xff00) >> 8);
output += String.fromCharCode(buffer & 0xff);
buffer = accumulatedBits = 0;
}
}
if (accumulatedBits === 12) {
buffer >>= 4;
output += String.fromCharCode(buffer);
} else if (accumulatedBits === 18) {
buffer >>= 2;
output += String.fromCharCode((buffer & 0xff00) >> 8);
output += String.fromCharCode(buffer & 0xff);
}
return output;
}
function btoa(s) {
if (arguments.length === 0) {
throw new TypeError("1 argument required, but only 0 present.");
}
const keystr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function btoaLookup(index) {
if (index >= 0 && index < 64) {
return keystr[index];
}
return undefined;
}
let i;
s = `${s}`;
for (i = 0; i < s.length; i++) {
if (s.charCodeAt(i) > 255) {
return null;
}
}
let out = "";
for (i = 0; i < s.length; i += 3) {
const groupsOfSix = [undefined, undefined, undefined, undefined];
groupsOfSix[0] = s.charCodeAt(i) >> 2;
groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
if (s.length > i + 1) {
groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
}
if (s.length > i + 2) {
groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
}
for (let j = 0; j < groupsOfSix.length; j++) {
if (typeof groupsOfSix[j] === "undefined") {
out += "=";
} else {
out += btoaLookup(groupsOfSix[j]);
}
}
}
return out;
}
// provided by: @Trung0246 on Github
/**
* Readable refactor of crc_vrf() with identical output to the original.
* - Uses byte arrays throughout.
* - Consolidates repeated transform logic.
* - Adds clear naming and comments.
*
* Example check (must be true):
* console.log(
* crc_vrf("67890@ The quick brown fox jumps over the lazy dog @12345")
* === "ZBYeRCjYBk0tkZnKW4kTuWBYw-81e-csvu6v17UY4zchviixt67VJ_tjpFEsOXB-a8X4ZFpDoDbPq8ms-7IyN95vmLVdP5vWSoTAl4ZbIBE8xijci8emrkdEYmArOPMUq5KAc3KEabUzHkNwjBtwvs0fQR7nDpI"
* );
*/
// Node/browser-friendly base64 helpers
const atob_ = typeof atob === "function"
? atob
: (b64) => Buffer.from(b64, "base64").toString("binary");
const btoa_ = typeof btoa === "function"
? btoa
: (bin) => Buffer.from(bin, "binary").toString("base64");
// Byte helpers
const toBytes = (str) => Array.from(str, (c) => c.charCodeAt(0) & 0xff);
const fromBytes = (bytes) => bytes.map((b) => String.fromCharCode(b & 0xff)).join("");
// RC4 over byte arrays (key is a binary string)
function rc4Bytes(key, input) {
const s = Array.from({ length: 256 }, (_, i) => i);
let j = 0;
// KSA
for (let i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) & 0xff;
[s[i], s[j]] = [s[j], s[i]];
}
// PRGA
const out = new Array(input.length);
let i = 0;
j = 0;
for (let y = 0; y < input.length; y++) {
i = (i + 1) & 0xff;
j = (j + s[i]) & 0xff;
[s[i], s[j]] = [s[j], s[i]];
const k = s[(s[i] + s[j]) & 0xff];
out[y] = (input[y] ^ k) & 0xff;
}
return out;
}
// One generic “step” to remove repeated boilerplate.
function transform(input, initSeedBytes, prefixKeyString, prefixLen, schedule) {
const out = [];
for (let i = 0; i < input.length; i++) {
if (i < prefixLen) out.push(prefixKeyString.charCodeAt(i) & 0xff);
out.push(schedule[i % 10]((input[i] ^ initSeedBytes[i % 32]) & 0xff) & 0xff);
}
return out;
}
// 8-bit ops
const add8 = (n) => (c) => (c + n) & 0xff;
const sub8 = (n) => (c) => (c - n + 256) & 0xff;
const xor8 = (n) => (c) => (c ^ n) & 0xff;
const rotl8 = (n) => (c) => ((c << n) | (c >>> (8 - n))) & 0xff;
// Schedules for each step (10 ops each, indexed by i % 10)
const scheduleC = [
sub8(48), sub8(19), xor8(241), sub8(19), add8(223),
sub8(19), sub8(170), sub8(19), sub8(48), xor8(8),
];
const scheduleY = [
rotl8(4), add8(223), rotl8(4), xor8(163), sub8(48),
add8(82), add8(223), sub8(48), xor8(83), rotl8(4),
];
const scheduleB = [
sub8(19), add8(82), sub8(48), sub8(170), rotl8(4),
sub8(48), sub8(170), xor8(8), add8(82), xor8(163),
];
const scheduleJ = [
add8(223), rotl8(4), add8(223), xor8(83), sub8(19),
add8(223), sub8(170), add8(223), sub8(170), xor8(83),
];
const scheduleE = [
add8(82), xor8(83), xor8(163), add8(82), sub8(170),
xor8(8), xor8(241), add8(82), add8(176), rotl8(4),
];
function base64UrlEncodeBytes(bytes) {
const std = btoa_(fromBytes(bytes));
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function bytesFromBase64(b64) {
return toBytes(atob_(b64));
}
// Constants — grouped logically and left as-is (base64) for clarity.
const CONST = {
rc4Keys: {
l: "u8cBwTi1CM4XE3BkwG5Ble3AxWgnhKiXD9Cr279yNW0=",
g: "t00NOJ/Fl3wZtez1xU6/YvcWDoXzjrDHJLL2r/IWgcY=",
B: "S7I+968ZY4Fo3sLVNH/ExCNq7gjuOHjSRgSqh6SsPJc=",
m: "7D4Q8i8dApRj6UWxXbIBEa1UqvjI+8W0UvPH9talJK8=",
F: "0JsmfWZA1kwZeWLk5gfV5g41lwLL72wHbam5ZPfnOVE=",
},
seeds32: {
A: "pGjzSCtS4izckNAOhrY5unJnO2E1VbrU+tXRYG24vTo=",
V: "dFcKX9Qpu7mt/AD6mb1QF4w+KqHTKmdiqp7penubAKI=",
N: "owp1QIY/kBiRWrRn9TLN2CdZsLeejzHhfJwdiQMjg3w=",
P: "H1XbRvXOvZAhyyPaO68vgIUgdAHn68Y6mrwkpIpEue8=",
k: "2Nmobf/mpQ7+Dxq1/olPSDj3xV8PZkPbKaucJvVckL0=",
},
prefixKeys: {
O: "Rowe+rg/0g==",
v: "8cULcnOMJVY8AA==",
L: "n2+Og2Gth8Hh",
p: "aRpvzH+yoA==",
W: "ZB4oBi0=",
},
};
function crc_vrf(input) {
// Stage 0: normalize to URI-encoded bytes
let bytes = toBytes(encodeURIComponent(input));
// RC4 1
bytes = rc4Bytes(atob_(CONST.rc4Keys.l), bytes);
// Step C1
bytes = transform(
bytes,
bytesFromBase64(CONST.seeds32.A),
atob_(CONST.prefixKeys.O),
7,
scheduleC
);
// RC4 2
bytes = rc4Bytes(atob_(CONST.rc4Keys.g), bytes);
// Step Y
bytes = transform(
bytes,
bytesFromBase64(CONST.seeds32.V),
atob_(CONST.prefixKeys.v),
10,
scheduleY
);
// RC4 3
bytes = rc4Bytes(atob_(CONST.rc4Keys.B), bytes);
// Step B
bytes = transform(
bytes,
bytesFromBase64(CONST.seeds32.N),
atob_(CONST.prefixKeys.L),
9,
scheduleB
);
// RC4 4
bytes = rc4Bytes(atob_(CONST.rc4Keys.m), bytes);
// Step J
bytes = transform(
bytes,
bytesFromBase64(CONST.seeds32.P),
atob_(CONST.prefixKeys.p),
7,
scheduleJ
);
// RC4 5
bytes = rc4Bytes(atob_(CONST.rc4Keys.F), bytes);
// Step E
bytes = transform(
bytes,
bytesFromBase64(CONST.seeds32.k),
atob_(CONST.prefixKeys.W),
5,
scheduleE
);
// Base64URL
return base64UrlEncodeBytes(bytes);
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaFire'
extClass = '.MangaFireFactory'
extVersionCode = 13
extVersionCode = 14
isNsfw = true
}

View File

@ -11,8 +11,8 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -24,22 +24,30 @@ import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.charset
import okio.Buffer
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.ByteArrayInputStream
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class MangaFire(
override val lang: String,
@ -52,7 +60,25 @@ class MangaFire(
override val supportsLatest = true
private val preferences by getPreferencesLazy()
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(ImageInterceptor)
.apply {
val naiveTrustManager = @SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) = Unit
}
val insecureSocketFactory = SSLContext.getInstance("SSL").apply {
val trustAllCerts = arrayOf<TrustManager>(naiveTrustManager)
init(null, trustAllCerts, SecureRandom())
}.socketFactory
sslSocketFactory(insecureSocketFactory, naiveTrustManager)
hostnameVerifier { _, _ -> true }
}
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
@ -82,15 +108,6 @@ class MangaFire(
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
private val vrfScript by lazy {
val vrf = this::class.java.getResourceAsStream("/assets/vrf.js")!!
.bufferedReader()
.readText()
QuickJs.create().use {
it.compile(vrf, "vrf")
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
@ -109,10 +126,7 @@ class MangaFire(
addQueryParameter("page", page.toString())
if (query.isNotBlank()) {
val vrf = QuickJs.create().use {
it.execute(vrfScript)
it.evaluate("crc_vrf(\"${query.trim()}\")") as String
}
val vrf = VrfGenerator.generate(query.trim())
addQueryParameter("vrf", vrf)
}
}.build()
@ -255,11 +269,11 @@ class MangaFire(
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
var ajaxUrl: String? = null
var errorMessage: String? = null
val context = Injekt.get<Application>()
val handler = Handler(Looper.getMainLooper())
val latch = CountDownLatch(1)
val emptyWebViewResponse = WebResourceResponse("text/html", "utf-8", Buffer().inputStream())
var webView: WebView? = null
handler.post {
@ -270,12 +284,10 @@ class MangaFire(
domStorageEnabled = true
databaseEnabled = true
blockNetworkImage = true
userAgentString = headers["User-Agent"]
}
webview.webViewClient = object : WebViewClient() {
private val ajaxCalls = setOf("ajax/read/chapter", "ajax/read/volume")
private val emptyWebViewResponse = WebResourceResponse("text/html", "utf-8", ByteArrayInputStream(" ".toByteArray()))
override fun shouldInterceptRequest(
view: WebView,
@ -287,14 +299,14 @@ class MangaFire(
if (url.host.orEmpty().contains("mfcdn.cc") && url.pathSegments.lastOrNull().orEmpty().contains("js")) {
Log.d(name, "allowed: $url")
return super.shouldInterceptRequest(view, request)
return fetchWebResource(request)
}
// allow jquery script
if (url.host.orEmpty().contains("cloudflare.com") && url.encodedPath.orEmpty().contains("jquery")) {
Log.d(name, "allowed: $url")
return super.shouldInterceptRequest(view, request)
return fetchWebResource(request)
}
// allow ajax/read calls and intercept ajax/read/chapter or ajax/read/volume
@ -304,15 +316,13 @@ class MangaFire(
if (url.getQueryParameter("vrf") != null) {
ajaxUrl = url.toString()
} else {
errorMessage = "vrf not found"
}
latch.countDown()
} else {
// need to allow other call to ajax/read
Log.d(name, "allowed: $url")
return super.shouldInterceptRequest(view, request)
return fetchWebResource(request)
}
}
@ -333,7 +343,7 @@ class MangaFire(
if (latch.count == 1L) {
throw Exception("Timeout getting vrf token")
} else if (ajaxUrl == null) {
throw Exception(errorMessage ?: "Unknown Error")
throw Exception("Unable to find vrf token")
}
return client.newCall(GET(ajaxUrl!!, headers)).execute()
@ -347,6 +357,31 @@ class MangaFire(
}
}
private fun fetchWebResource(request: WebResourceRequest): WebResourceResponse = runBlocking(Dispatchers.IO) {
val okhttpRequest = Request.Builder().apply {
url(request.url.toString())
headers(headers)
val skipHeaders = setOf("referer", "user-agent", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "x-requested-with")
for ((name, value) in request.requestHeaders) {
if (skipHeaders.contains(name.lowercase())) continue
addHeader(name, value)
}
}.build()
client.newCall(okhttpRequest).await().use { response ->
val mediaType = response.body.contentType()
WebResourceResponse(
mediaType?.let { "${it.type}/${it.subtype}" },
mediaType?.charset()?.name(),
Buffer().readFrom(
response.body.byteStream(),
).inputStream(),
)
}
}
@Serializable
class PageListDto(private val images: List<List<JsonPrimitive>>) {
val pages

View File

@ -0,0 +1,186 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import android.util.Base64
/**
* Original script by @Trung0246 on Github
*/
object VrfGenerator {
private fun atob(data: String): ByteArray = Base64.decode(data, Base64.DEFAULT)
private fun btoa(data: ByteArray): String = Base64.encodeToString(data, Base64.DEFAULT)
private fun rc4(key: ByteArray, input: ByteArray): ByteArray {
val s = IntArray(256) { it }
var j = 0
// KSA
for (i in 0..255) {
j = (j + s[i] + key[i % key.size].toInt().and(0xFF)) and 0xFF
val temp = s[i]
s[i] = s[j]
s[j] = temp
}
// PRGA
val output = ByteArray(input.size)
var i = 0
j = 0
for (y in input.indices) {
i = (i + 1) and 0xFF
j = (j + s[i]) and 0xFF
val temp = s[i]
s[i] = s[j]
s[j] = temp
val k = s[(s[i] + s[j]) and 0xFF]
output[y] = (input[y].toInt() xor k).toByte()
}
return output
}
private fun transform(
input: ByteArray,
initSeedBytes: ByteArray,
prefixKeyBytes: ByteArray,
prefixLen: Int,
schedule: List<(Int) -> Int>,
): ByteArray {
val out = mutableListOf<Byte>()
for (i in input.indices) {
if (i < prefixLen) {
out.add(prefixKeyBytes[i])
}
val transformed = schedule[i % 10](
(input[i].toInt() xor initSeedBytes[i % 32].toInt()) and 0xFF,
) and 0xFF
out.add(transformed.toByte())
}
return out.toByteArray()
}
private val scheduleC = listOf<(Int) -> Int>(
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c xor 241) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
)
private val scheduleY = listOf<(Int) -> Int>(
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c xor 163) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
)
private val scheduleB = listOf<(Int) -> Int>(
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c xor 163) and 0xFF },
)
private val scheduleJ = listOf<(Int) -> Int>(
{ c -> (c + 223) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 83) and 0xFF },
)
private val scheduleE = listOf<(Int) -> Int>(
{ c -> (c + 82) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> (c xor 163) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
{ c -> (c xor 241) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c + 176) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
)
private val rc4Keys = mapOf(
"l" to "u8cBwTi1CM4XE3BkwG5Ble3AxWgnhKiXD9Cr279yNW0=",
"g" to "t00NOJ/Fl3wZtez1xU6/YvcWDoXzjrDHJLL2r/IWgcY=",
"B" to "S7I+968ZY4Fo3sLVNH/ExCNq7gjuOHjSRgSqh6SsPJc=",
"m" to "7D4Q8i8dApRj6UWxXbIBEa1UqvjI+8W0UvPH9talJK8=",
"F" to "0JsmfWZA1kwZeWLk5gfV5g41lwLL72wHbam5ZPfnOVE=",
)
private val seeds32 = mapOf(
"A" to "pGjzSCtS4izckNAOhrY5unJnO2E1VbrU+tXRYG24vTo=",
"V" to "dFcKX9Qpu7mt/AD6mb1QF4w+KqHTKmdiqp7penubAKI=",
"N" to "owp1QIY/kBiRWrRn9TLN2CdZsLeejzHhfJwdiQMjg3w=",
"P" to "H1XbRvXOvZAhyyPaO68vgIUgdAHn68Y6mrwkpIpEue8=",
"k" to "2Nmobf/mpQ7+Dxq1/olPSDj3xV8PZkPbKaucJvVckL0=",
)
private val prefixKeys = mapOf(
"O" to "Rowe+rg/0g==",
"v" to "8cULcnOMJVY8AA==",
"L" to "n2+Og2Gth8Hh",
"p" to "aRpvzH+yoA==",
"W" to "ZB4oBi0=",
)
fun generate(input: String): String {
var bytes = input.toByteArray()
// RC4 1
bytes = rc4(atob(rc4Keys["l"]!!), bytes)
// Step C1
bytes = transform(bytes, atob(seeds32["A"]!!), atob(prefixKeys["O"]!!), 7, scheduleC)
// RC4 2
bytes = rc4(atob(rc4Keys["g"]!!), bytes)
// Step Y
bytes = transform(bytes, atob(seeds32["V"]!!), atob(prefixKeys["v"]!!), 10, scheduleY)
// RC4 3
bytes = rc4(atob(rc4Keys["B"]!!), bytes)
// Step B
bytes = transform(bytes, atob(seeds32["N"]!!), atob(prefixKeys["L"]!!), 9, scheduleB)
// RC4 4
bytes = rc4(atob(rc4Keys["m"]!!), bytes)
// Step J
bytes = transform(bytes, atob(seeds32["P"]!!), atob(prefixKeys["p"]!!), 7, scheduleJ)
// RC4 5
bytes = rc4(atob(rc4Keys["F"]!!), bytes)
// Step E
bytes = transform(bytes, atob(seeds32["k"]!!), atob(prefixKeys["W"]!!), 5, scheduleE)
// Base64URL encode
return btoa(bytes)
.replace("+", "-")
.replace("/", "_")
.replace("=", "")
}
}