Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
],
"license": "MIT",
"dependencies": {
"@types/node": "^22.15.2"
"@types/node": "^22.17.0"
},
"devDependencies": {
"esbuild": "^0.25.3",
"eslint": "^9.25.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0"
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.38.0"
}
}
146 changes: 146 additions & 0 deletions sources/src/decryptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* eslint-disable @typescript-eslint/naming-convention */
// Generated by ChatGPT o4-mini-high and some manual edits

// 1. 커스텀 Base64 디코더 (알파벳은 원본 코드의 것)
const CUSTOM_B64_ALPHABET = 'LoCpFhMUgtDnQXE6kBz_y-7Hb8SmIjaJO2l30WixfPKV9Auv1Rq4ZY5wdseTNrGc'
const B64_REVERSE = (() => {
const m = Object.create(null)
for (let i = 0; i < CUSTOM_B64_ALPHABET.length; i++) {
m[CUSTOM_B64_ALPHABET[i]] = i
}
return m
})()

/**
* Base64-like 문자열을 바이트로 디코딩.
* 패딩 문자가 없고 길이에 따라 끝이 잘린 상태를 처리.
*/
function decodeCustomBase64(str) {
const bytes = []
let i = 0
while (i + 4 <= str.length) {
const v0 = B64_REVERSE[str[i++]]
const v1 = B64_REVERSE[str[i++]]
const v2 = B64_REVERSE[str[i++]]
const v3 = B64_REVERSE[str[i++]]
bytes.push((v0 << 2) | (v1 >> 4))
bytes.push(((v1 & 0xf) << 4) | (v2 >> 2))
bytes.push(((v2 & 0x3) << 6) | v3)
}
// 남은 2~3 글자 처리 (패딩이 생략된 경우)
const rem = str.length - i
if (rem === 2) {
const v0 = B64_REVERSE[str[i++]]
const v1 = B64_REVERSE[str[i++]]
bytes.push((v0 << 2) | (v1 >> 4))
} else if (rem === 3) {
const v0 = B64_REVERSE[str[i++]]
const v1 = B64_REVERSE[str[i++]]
const v2 = B64_REVERSE[str[i++]]
bytes.push((v0 << 2) | (v1 >> 4))
bytes.push(((v1 & 0xf) << 4) | (v2 >> 2))
}
return new Uint8Array(bytes)
}

// 2. RC4 구현 (원본과 동일한 KSA/PRGA)
function rc4(keyBytes, dataBytes) {
const S = new Uint8Array(256)
for (let i = 0; i < 256; i++) S[i] = i
let j = 0
// KSA: keyBytes can be any length; original used 32-byte array
for (let i = 0; i < 256; i++) {
j = (j + S[i] + keyBytes[i % keyBytes.length]) & 0xff;
[S[i], S[j]] = [S[j], S[i]]
}
// PRGA
let i = 0
j = 0
const out = new Uint8Array(dataBytes.length)
for (let k = 0; k < dataBytes.length; k++) {
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[k] = dataBytes[k] ^ K
}
return out
}

// --- 장소표시자: key/nonce로부터 실제 값을 도출하는 함수들 ---

/**
* 예시: key + nonce로부터 헤더 마스크(원래 17바이트)를 생성.
* 실제 원본 구현을 모르면 이 부분을 대체해야 함.
* 여기서는 단순히 고정값 예시를 사용하는 형태.
*/
function deriveHeaderMask() {
// 만약 실제가 MD5(key + nonce) 기반이라면 여기서 해시를 계산하고
// 앞 17 바이트를 사용. 이 예에서는 고정 "2efe3d23aec798e47" ascii.
const fixedAscii = '2efe3d23aec798e47'
const arr = new Uint8Array(fixedAscii.length)
for (let i = 0; i < fixedAscii.length; i++) {
arr[i] = fixedAscii.charCodeAt(i)
}
return arr // length 17
}

/**
* 예시: key + nonce로부터 RC4 키(32바이트)를 만드는 함수.
* 실제 구현에 따라 대체할 것. (예: HMAC-SHA256(key, nonce)로 확장 등)
* 여기선 원본 코드에 쓰인 고정 32바이트를 그대로 사용.
*/
function deriveRc4Key() {
// 원본 코드의 고정 배열:
return new Uint8Array([
37, 67, 13, 50, 127, 0, 34, 98, 208, 44, 155, 179, 137, 222, 69, 119,
229, 72, 43, 65, 30, 49, 79, 111, 240, 221, 12, 50, 44, 30, 220, 245
])
}

// --- 최종 복원 함수 ---

/**
* "/i/..." 토큰으로부터 원래 경로(물음표 이전)를 복원.
* @param {string} token "/i/..." 형태. 뒤에 쿼리가 붙어 있을 수 있음.
* @param {*} key 사용자 제공 key (복원 로직에 맞게 derive 함수 내에서 사용)
* @param {*} nonce 사용자 제공 nonce
* @returns {string} 복원된 원래 경로
*/
export function decodeIResult(token: string, key: string, nonce: string): string {
// 1. "/i/" 제거, 쿼리 분리
if (token.startsWith('/i/')) token = token.slice(3)
let queryPart = ''
const qi = token.indexOf('?')
if (qi !== -1) {
queryPart = token.slice(qi) // 그대로 이어붙일 수 있음
token = token.slice(0, qi)
}

// 2. 커스텀 base64 디코딩
const decoded = decodeCustomBase64(token)
if (decoded.length < 1 + 17) {
throw new Error('디코딩 결과가 너무 짧습니다.')
}

// 3. 길이와 헤더 검증
const pathLen = decoded[0] // 원래 경로 길이 (mod 256)
const headerBytes = decoded.slice(1, 1 + 17)
const expectedHeader = deriveHeaderMask()
for (let i = 0; i < 17; i++) {
if (headerBytes[i] !== (expectedHeader[i] ^ pathLen)) {
throw new Error('헤더 검증 실패: key/nonce가 잘못됐거나 토큰이 변조됨.')
}
}

// 4. 암호화된 경로 부분 복원 (RC4)
const cipherPath = decoded.slice(1 + 17, 1 + 17 + pathLen)
const rc4Key = deriveRc4Key()
const plainPathBytes = rc4(rc4Key, cipherPath)

// 5. UTF-8 디코딩
const decoder = new TextDecoder()
const path = decoder.decode(plainPathBytes)

return path + queryPart // 필요 시 원래 쿼리도 복원
}
28 changes: 28 additions & 0 deletions sources/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { decodeIResult } from './decryptor.js'

type unsafeWindow = typeof window
// eslint-disable-next-line @typescript-eslint/naming-convention
declare const unsafeWindow: unsafeWindow
Expand Down Expand Up @@ -82,4 +84,30 @@ Win.Function.prototype.bind = new Proxy(Win.Function.prototype.bind, {
}
return Reflect.apply(Target, ThisArg, Args)
}
})

Win.fetch = new Proxy(Win.fetch, {
async apply(Target: typeof fetch, ThisArg: typeof Win, Args: Parameters<typeof fetch>) {
let AwaitResult = Reflect.apply(Target, ThisArg, Args)
let Result = await AwaitResult
if (Result.headers.has('x-namuwiki-key') && Args[0] instanceof Request && Args[0].headers.has('x-namuwiki-nonce') &&
decodeIResult(Args[0].url, Result.headers.get('x-namuwiki-key'), Args[0].headers.get('x-namuwiki-nonce'))) {
return new Promise(() => {})
}
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && Args[1].headers instanceof Headers && Args[1].headers.has('x-namuwiki-nonce') &&
decodeIResult(Args[0] instanceof URL ? Args[0].pathname : Args[0], Result.headers.get('x-namuwiki-key'), Args[1].headers.get('x-namuwiki-nonce'))) {
return new Promise(() => {})
}
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && !(Args[1].headers instanceof Headers) &&
Array.isArray(Args[1].headers) && Args[1].headers.some(InnerHeader => InnerHeader[0] === 'x-namuwiki-nonce') &&
decodeIResult(Args[0] instanceof URL ? Args[0].pathname : Args[0], Result.headers.get('x-namuwiki-key'), Args[1].headers.find(InnerHeader => InnerHeader[0] === 'x-namuwiki-nonce')[1])) {
return new Promise(() => {})
}
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && !(Args[1].headers instanceof Headers) &&
!Array.isArray(Args[1].headers) && typeof Args[1].headers['x-namuwiki-nonce'] === 'string' &&
decodeIResult(Args[0] instanceof URL ? Args[0].pathname : Args[0], Result.headers.get('x-namuwiki-key'), Args[1].headers['x-namuwiki-nonce'])) {
return new Promise(() => {})
}
return Result
}
})
Loading