diff --git a/src/all/mangafire/assets/vrf.js b/src/all/mangafire/assets/vrf.js deleted file mode 100644 index daa91723d..000000000 --- a/src/all/mangafire/assets/vrf.js +++ /dev/null @@ -1,294 +0,0 @@ -/** - Copyright © 2019 W3C and Jeff Carpenter - - 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); -} diff --git a/src/all/mangafire/build.gradle b/src/all/mangafire/build.gradle index 9ac00997b..e097562e3 100644 --- a/src/all/mangafire/build.gradle +++ b/src/all/mangafire/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'MangaFire' extClass = '.MangaFireFactory' - extVersionCode = 13 + extVersionCode = 14 isNsfw = true } diff --git a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt index 1448ce6b9..48e0c7eb0 100644 --- a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt +++ b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt @@ -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 = emptyArray() + override fun checkClientTrusted(certs: Array, authType: String) = Unit + override fun checkServerTrusted(certs: Array, authType: String) = Unit + } + + val insecureSocketFactory = SSLContext.getInstance("SSL").apply { + val trustAllCerts = arrayOf(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 { val document = response.asJsoup() var ajaxUrl: String? = null - var errorMessage: String? = null val context = Injekt.get() 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>) { val pages diff --git a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/VrfGenerator.kt b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/VrfGenerator.kt new file mode 100644 index 000000000..fb562a5ed --- /dev/null +++ b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/VrfGenerator.kt @@ -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() + 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("=", "") + } +}