/** * 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//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; }