filemoon (+ mirrory kerapoxy/lvturbo/emturbovid/bysezoxexe/bysezejataos)
nie umarł — ~2026-05 zrobił rebrand na Vite SPA "Byse Frontend". Stary
P.A.C.K.E.R.-JWPlayer embed zniknął, więc backend uznał go za martwego i
wpisał na DEAD_HOSTER_RE. RE bundla index-ChwZgmXV.js (2026-05-22):
POST /api/videos/<code>/embed/playback body {"fingerprint":{}}
→ {"playback":{"key_parts":[..],"iv":..,"payload":..}}
→ key=concat(b64url(key_parts)); AES-256-GCM(key,iv,payload) → JSON
→ sources[*].url = HLS master.m3u8
Browser-attestation jest opcjonalny — pusty fingerprint wystarcza.
Stream URL jest IP-bound (token wiąże się z IP requestera), więc resolve
musi iść z urządzenia użytkownika (jak doodstream.ts / packerHoster.ts).
- mobile/src/lib/aesGcm.ts — pure-JS AES-256-GCM decrypt (RN/Hermes nie
ma Web Crypto); S-box liczony z GF(2^8), GHASH weryfikuje tag.
Zweryfikowane przeciw cryptography (Python) na 2 payloadach.
- mobile/src/lib/filemoonHoster.ts — resolver: POST playback → decrypt →
pick best source. E2E test: filemoon.to/e + /d + bysezoxexe.com mirror.
- PlayerScreen: filemoon w resolve useEffect obok doodstream/packer.
- backend: filemoon poza DEAD_HOSTER_RE; hoster.py early-return → przelot
jako type='hoster' do mobile resolvera (server-side resolve bezcelowy,
bo URL IP-bound do VPS).
- direct_scrapers: poprawiony błędny komentarz "filemoon shutdown".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
244 lines
8 KiB
TypeScript
244 lines
8 KiB
TypeScript
/**
|
|
* Minimal pure-JS AES-256-GCM decryption.
|
|
*
|
|
* Powód: React Native (Hermes) nie ma Web Crypto (`crypto.subtle`), a projekt
|
|
* nie linkuje natywnego crypto modułu. `filemoonHoster.ts` musi rozszyfrować
|
|
* AES-256-GCM payload zwracany przez filemoon `/api/videos/<code>/embed/playback`.
|
|
* Czysty JS = zmiana ships przez OTA update (bez rebuilda APK).
|
|
*
|
|
* Zakres: tylko DECRYPT, klucz 256-bit, IV 96-bit (GCM standard), 128-bit tag
|
|
* doklejony na końcu ciphertextu (konwencja Web Crypto / Go `crypto/cipher`).
|
|
* Tablica S-box liczona z GF(2^8) przy load module — zero hardkodowanych
|
|
* 256-elementowych literałów (mniej miejsca na literówkę). GHASH weryfikuje tag.
|
|
*
|
|
* Referencje: NIST SP 800-38D (GCM), FIPS-197 (AES).
|
|
*/
|
|
|
|
// ---- GF(2^8) — xtime + tablice exp/log + S-box ----
|
|
|
|
function xtime(b: number): number {
|
|
return ((b << 1) ^ (b & 0x80 ? 0x1b : 0)) & 0xff;
|
|
}
|
|
|
|
const SBOX = new Uint8Array(256);
|
|
|
|
(function initSbox() {
|
|
// log/antilog w GF(2^8) z generatorem 3 (0x03).
|
|
const exp = new Uint8Array(256);
|
|
const log = new Uint8Array(256);
|
|
let x = 1;
|
|
for (let i = 0; i < 255; i++) {
|
|
exp[i] = x;
|
|
log[x] = i;
|
|
x ^= xtime(x); // x = x*2 ^ x = x*3
|
|
}
|
|
// S-box: odwrotność multiplikatywna + transformacja afiniczna.
|
|
for (let a = 0; a < 256; a++) {
|
|
let inv = a === 0 ? 0 : exp[(255 - log[a]) % 255];
|
|
let s = inv;
|
|
let r = inv;
|
|
for (let i = 0; i < 4; i++) {
|
|
s = ((s << 1) | (s >>> 7)) & 0xff;
|
|
r ^= s;
|
|
}
|
|
SBOX[a] = r ^ 0x63;
|
|
}
|
|
})();
|
|
|
|
// ---- AES-256 key schedule (Nk=8, Nr=14 → 240 bajtów round keys) ----
|
|
|
|
const NR = 14;
|
|
|
|
function expandKey(key: Uint8Array): Uint8Array {
|
|
if (key.length !== 32) throw new Error('aesGcm: key must be 32 bytes (AES-256)');
|
|
const Nk = 8;
|
|
const total = 4 * (NR + 1); // 60 słów
|
|
const w = new Uint8Array(total * 4); // 240 bajtów
|
|
w.set(key);
|
|
let rc = 1;
|
|
for (let i = Nk; i < total; i++) {
|
|
let t0 = w[(i - 1) * 4];
|
|
let t1 = w[(i - 1) * 4 + 1];
|
|
let t2 = w[(i - 1) * 4 + 2];
|
|
let t3 = w[(i - 1) * 4 + 3];
|
|
if (i % Nk === 0) {
|
|
// RotWord + SubWord + Rcon
|
|
const tmp = t0;
|
|
t0 = SBOX[t1] ^ rc;
|
|
t1 = SBOX[t2];
|
|
t2 = SBOX[t3];
|
|
t3 = SBOX[tmp];
|
|
rc = xtime(rc);
|
|
} else if (i % Nk === 4) {
|
|
// AES-256: dodatkowy SubWord co 4 słowa
|
|
t0 = SBOX[t0];
|
|
t1 = SBOX[t1];
|
|
t2 = SBOX[t2];
|
|
t3 = SBOX[t3];
|
|
}
|
|
w[i * 4] = w[(i - Nk) * 4] ^ t0;
|
|
w[i * 4 + 1] = w[(i - Nk) * 4 + 1] ^ t1;
|
|
w[i * 4 + 2] = w[(i - Nk) * 4 + 2] ^ t2;
|
|
w[i * 4 + 3] = w[(i - Nk) * 4 + 3] ^ t3;
|
|
}
|
|
return w;
|
|
}
|
|
|
|
function m3(b: number): number {
|
|
return xtime(b) ^ b;
|
|
}
|
|
|
|
// Szyfrowanie pojedynczego 16-bajtowego bloku. GCM używa tylko AES-encrypt
|
|
// (zarówno do keystreamu CTR jak i do hashowania) — AES-decrypt nie jest potrzebny.
|
|
function encryptBlock(rk: Uint8Array, input: Uint8Array): Uint8Array {
|
|
const s = new Uint8Array(16);
|
|
s.set(input);
|
|
// AddRoundKey (runda 0)
|
|
for (let i = 0; i < 16; i++) s[i] ^= rk[i];
|
|
for (let round = 1; round <= NR; round++) {
|
|
// SubBytes
|
|
for (let i = 0; i < 16; i++) s[i] = SBOX[s[i]];
|
|
// ShiftRows (state column-major: bajt (row r, col c) = s[r + 4c])
|
|
let t = s[1];
|
|
s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;
|
|
t = s[2]; s[2] = s[10]; s[10] = t;
|
|
t = s[6]; s[6] = s[14]; s[14] = t;
|
|
t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;
|
|
// MixColumns (pomijane w ostatniej rundzie)
|
|
if (round < NR) {
|
|
for (let c = 0; c < 4; c++) {
|
|
const a0 = s[4 * c], a1 = s[4 * c + 1], a2 = s[4 * c + 2], a3 = s[4 * c + 3];
|
|
s[4 * c] = xtime(a0) ^ m3(a1) ^ a2 ^ a3;
|
|
s[4 * c + 1] = a0 ^ xtime(a1) ^ m3(a2) ^ a3;
|
|
s[4 * c + 2] = a0 ^ a1 ^ xtime(a2) ^ m3(a3);
|
|
s[4 * c + 3] = m3(a0) ^ a1 ^ a2 ^ xtime(a3);
|
|
}
|
|
}
|
|
// AddRoundKey
|
|
for (let i = 0; i < 16; i++) s[i] ^= rk[round * 16 + i];
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// ---- GCM ----
|
|
|
|
// Mnożenie w GF(2^128) wg konwencji GCM (bit-reversed, wielomian R = 0xe1<<120).
|
|
function gmult(X: Uint8Array, Y: Uint8Array): Uint8Array {
|
|
const Z = new Uint8Array(16);
|
|
const V = Y.slice();
|
|
for (let i = 0; i < 128; i++) {
|
|
const bit = (X[i >>> 3] >>> (7 - (i & 7))) & 1;
|
|
if (bit) for (let j = 0; j < 16; j++) Z[j] ^= V[j];
|
|
const lsb = V[15] & 1;
|
|
for (let j = 15; j > 0; j--) V[j] = ((V[j] >>> 1) | ((V[j - 1] & 1) << 7)) & 0xff;
|
|
V[0] = V[0] >>> 1;
|
|
if (lsb) V[0] ^= 0xe1;
|
|
}
|
|
return Z;
|
|
}
|
|
|
|
// GHASH: X ← (X ^ block) · H, blok po bloku (16 bajtów, padding zerami).
|
|
function ghashUpdate(X: Uint8Array, H: Uint8Array, block: Uint8Array): Uint8Array {
|
|
const t = new Uint8Array(16);
|
|
for (let i = 0; i < 16; i++) t[i] = X[i] ^ (block[i] ?? 0);
|
|
return gmult(t, H);
|
|
}
|
|
|
|
function inc32(block: Uint8Array): void {
|
|
// inkrementacja ostatnich 4 bajtów jako big-endian uint32
|
|
for (let i = 15; i >= 12; i--) {
|
|
block[i] = (block[i] + 1) & 0xff;
|
|
if (block[i] !== 0) break;
|
|
}
|
|
}
|
|
|
|
function ct16Eq(a: Uint8Array, b: Uint8Array): boolean {
|
|
// porównanie w stałym czasie (16 bajtów)
|
|
let d = 0;
|
|
for (let i = 0; i < 16; i++) d |= a[i] ^ b[i];
|
|
return d === 0;
|
|
}
|
|
|
|
/**
|
|
* Rozszyfruj AES-256-GCM. `ciphertextWithTag` = ciphertext || 16-bajtowy tag.
|
|
* Rzuca wyjątek przy niezgodności tagu albo złych długościach.
|
|
*/
|
|
export function aesGcmDecrypt(
|
|
key: Uint8Array,
|
|
iv: Uint8Array,
|
|
ciphertextWithTag: Uint8Array,
|
|
): Uint8Array {
|
|
if (iv.length !== 12) throw new Error('aesGcm: IV must be 12 bytes (96-bit)');
|
|
if (ciphertextWithTag.length < 16) throw new Error('aesGcm: payload shorter than tag');
|
|
|
|
const rk = expandKey(key);
|
|
const ctLen = ciphertextWithTag.length - 16;
|
|
const ct = ciphertextWithTag.subarray(0, ctLen);
|
|
const tag = ciphertextWithTag.subarray(ctLen);
|
|
|
|
// H = AES_K(0^128)
|
|
const H = encryptBlock(rk, new Uint8Array(16));
|
|
|
|
// J0 = IV || 0x00000001
|
|
const J0 = new Uint8Array(16);
|
|
J0.set(iv);
|
|
J0[15] = 1;
|
|
|
|
// GHASH nad ciphertextem (AAD puste).
|
|
let X = new Uint8Array(16);
|
|
for (let off = 0; off < ctLen; off += 16) {
|
|
const block = ct.subarray(off, Math.min(off + 16, ctLen));
|
|
X = ghashUpdate(X, H, block);
|
|
}
|
|
// Blok długości: lenAAD (64-bit) || lenC (64-bit), oba w bitach.
|
|
const lenBlock = new Uint8Array(16);
|
|
const ctBits = ctLen * 8;
|
|
// lenAAD = 0 → bajty 0..7 zostają zerowe. lenC w bajtach 8..15 (big-endian).
|
|
lenBlock[15] = ctBits & 0xff;
|
|
lenBlock[14] = (ctBits >>> 8) & 0xff;
|
|
lenBlock[13] = (ctBits >>> 16) & 0xff;
|
|
lenBlock[12] = (ctBits >>> 24) & 0xff;
|
|
// ctBits > 2^32 nie wystąpi dla payloadów filemoon (kilka KB) — wyższe bajty 0.
|
|
X = ghashUpdate(X, H, lenBlock);
|
|
|
|
// Tag oczekiwany T = GHASH ^ AES_K(J0)
|
|
const ej0 = encryptBlock(rk, J0);
|
|
const expTag = new Uint8Array(16);
|
|
for (let i = 0; i < 16; i++) expTag[i] = X[i] ^ ej0[i];
|
|
if (!ct16Eq(expTag, tag)) throw new Error('aesGcm: authentication tag mismatch');
|
|
|
|
// Deszyfracja CTR — licznik startuje od inc32(J0).
|
|
const plain = new Uint8Array(ctLen);
|
|
const counter = J0.slice();
|
|
for (let off = 0; off < ctLen; off += 16) {
|
|
inc32(counter);
|
|
const ks = encryptBlock(rk, counter);
|
|
const n = Math.min(16, ctLen - off);
|
|
for (let i = 0; i < n; i++) plain[off + i] = ct[off + i] ^ ks[i];
|
|
}
|
|
return plain;
|
|
}
|
|
|
|
/** Dekoduje base64url (bez paddingu OK) do Uint8Array. Self-contained — nie
|
|
* zależy od `atob`/`Buffer` które w RN/Hermes bywają niedostępne. */
|
|
export function b64urlDecode(input: string): Uint8Array {
|
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
const lookup = new Int16Array(128).fill(-1);
|
|
for (let i = 0; i < alphabet.length; i++) lookup[alphabet.charCodeAt(i)] = i;
|
|
const clean = input.replace(/=+$/, '');
|
|
const out = new Uint8Array(Math.floor((clean.length * 6) / 8));
|
|
let bits = 0;
|
|
let acc = 0;
|
|
let oi = 0;
|
|
for (let i = 0; i < clean.length; i++) {
|
|
const v = lookup[clean.charCodeAt(i) & 0x7f];
|
|
if (v < 0) throw new Error('b64urlDecode: invalid character');
|
|
acc = (acc << 6) | v;
|
|
bits += 6;
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
out[oi++] = (acc >>> bits) & 0xff;
|
|
}
|
|
}
|
|
return out;
|
|
}
|