Skip to content

Commit 5d9d996

Browse files
committed
fear: add decryptor
1 parent 40fe556 commit 5d9d996

File tree

3 files changed

+179
-5
lines changed

3 files changed

+179
-5
lines changed

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737
],
3838
"license": "MIT",
3939
"dependencies": {
40-
"@types/node": "^22.15.2"
40+
"@types/node": "^22.17.0"
4141
},
4242
"devDependencies": {
43-
"esbuild": "^0.25.3",
44-
"eslint": "^9.25.1",
45-
"typescript": "^5.8.3",
46-
"typescript-eslint": "^8.31.0"
43+
"esbuild": "^0.25.8",
44+
"eslint": "^9.32.0",
45+
"typescript": "^5.9.2",
46+
"typescript-eslint": "^8.38.0"
4747
}
4848
}

sources/src/decryptor.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
// Generated by ChatGPT o4-mini-high and some manual edits
3+
4+
// 1. 커스텀 Base64 디코더 (알파벳은 원본 코드의 것)
5+
const CUSTOM_B64_ALPHABET = 'LoCpFhMUgtDnQXE6kBz_y-7Hb8SmIjaJO2l30WixfPKV9Auv1Rq4ZY5wdseTNrGc'
6+
const B64_REVERSE = (() => {
7+
const m = Object.create(null)
8+
for (let i = 0; i < CUSTOM_B64_ALPHABET.length; i++) {
9+
m[CUSTOM_B64_ALPHABET[i]] = i
10+
}
11+
return m
12+
})()
13+
14+
/**
15+
* Base64-like 문자열을 바이트로 디코딩.
16+
* 패딩 문자가 없고 길이에 따라 끝이 잘린 상태를 처리.
17+
*/
18+
function decodeCustomBase64(str) {
19+
const bytes = []
20+
let i = 0
21+
while (i + 4 <= str.length) {
22+
const v0 = B64_REVERSE[str[i++]]
23+
const v1 = B64_REVERSE[str[i++]]
24+
const v2 = B64_REVERSE[str[i++]]
25+
const v3 = B64_REVERSE[str[i++]]
26+
bytes.push((v0 << 2) | (v1 >> 4))
27+
bytes.push(((v1 & 0xf) << 4) | (v2 >> 2))
28+
bytes.push(((v2 & 0x3) << 6) | v3)
29+
}
30+
// 남은 2~3 글자 처리 (패딩이 생략된 경우)
31+
const rem = str.length - i
32+
if (rem === 2) {
33+
const v0 = B64_REVERSE[str[i++]]
34+
const v1 = B64_REVERSE[str[i++]]
35+
bytes.push((v0 << 2) | (v1 >> 4))
36+
} else if (rem === 3) {
37+
const v0 = B64_REVERSE[str[i++]]
38+
const v1 = B64_REVERSE[str[i++]]
39+
const v2 = B64_REVERSE[str[i++]]
40+
bytes.push((v0 << 2) | (v1 >> 4))
41+
bytes.push(((v1 & 0xf) << 4) | (v2 >> 2))
42+
}
43+
return new Uint8Array(bytes)
44+
}
45+
46+
// 2. RC4 구현 (원본과 동일한 KSA/PRGA)
47+
function rc4(keyBytes, dataBytes) {
48+
const S = new Uint8Array(256)
49+
for (let i = 0; i < 256; i++) S[i] = i
50+
let j = 0
51+
// KSA: keyBytes can be any length; original used 32-byte array
52+
for (let i = 0; i < 256; i++) {
53+
j = (j + S[i] + keyBytes[i % keyBytes.length]) & 0xff;
54+
[S[i], S[j]] = [S[j], S[i]]
55+
}
56+
// PRGA
57+
let i = 0
58+
j = 0
59+
const out = new Uint8Array(dataBytes.length)
60+
for (let k = 0; k < dataBytes.length; k++) {
61+
i = (i + 1) & 0xff
62+
j = (j + S[i]) & 0xff;
63+
[S[i], S[j]] = [S[j], S[i]]
64+
const K = S[(S[i] + S[j]) & 0xff]
65+
out[k] = dataBytes[k] ^ K
66+
}
67+
return out
68+
}
69+
70+
// --- 장소표시자: key/nonce로부터 실제 값을 도출하는 함수들 ---
71+
72+
/**
73+
* 예시: key + nonce로부터 헤더 마스크(원래 17바이트)를 생성.
74+
* 실제 원본 구현을 모르면 이 부분을 대체해야 함.
75+
* 여기서는 단순히 고정값 예시를 사용하는 형태.
76+
*/
77+
function deriveHeaderMask() {
78+
// 만약 실제가 MD5(key + nonce) 기반이라면 여기서 해시를 계산하고
79+
// 앞 17 바이트를 사용. 이 예에서는 고정 "2efe3d23aec798e47" ascii.
80+
const fixedAscii = '2efe3d23aec798e47'
81+
const arr = new Uint8Array(fixedAscii.length)
82+
for (let i = 0; i < fixedAscii.length; i++) {
83+
arr[i] = fixedAscii.charCodeAt(i)
84+
}
85+
return arr // length 17
86+
}
87+
88+
/**
89+
* 예시: key + nonce로부터 RC4 키(32바이트)를 만드는 함수.
90+
* 실제 구현에 따라 대체할 것. (예: HMAC-SHA256(key, nonce)로 확장 등)
91+
* 여기선 원본 코드에 쓰인 고정 32바이트를 그대로 사용.
92+
*/
93+
function deriveRc4Key() {
94+
// 원본 코드의 고정 배열:
95+
return new Uint8Array([
96+
37, 67, 13, 50, 127, 0, 34, 98, 208, 44, 155, 179, 137, 222, 69, 119,
97+
229, 72, 43, 65, 30, 49, 79, 111, 240, 221, 12, 50, 44, 30, 220, 245
98+
])
99+
}
100+
101+
// --- 최종 복원 함수 ---
102+
103+
/**
104+
* "/i/..." 토큰으로부터 원래 경로(물음표 이전)를 복원.
105+
* @param {string} token "/i/..." 형태. 뒤에 쿼리가 붙어 있을 수 있음.
106+
* @param {*} key 사용자 제공 key (복원 로직에 맞게 derive 함수 내에서 사용)
107+
* @param {*} nonce 사용자 제공 nonce
108+
* @returns {string} 복원된 원래 경로
109+
*/
110+
export function decodeIResult(token: string, key: string, nonce: string): string {
111+
// 1. "/i/" 제거, 쿼리 분리
112+
if (token.startsWith('/i/')) token = token.slice(3)
113+
let queryPart = ''
114+
const qi = token.indexOf('?')
115+
if (qi !== -1) {
116+
queryPart = token.slice(qi) // 그대로 이어붙일 수 있음
117+
token = token.slice(0, qi)
118+
}
119+
120+
// 2. 커스텀 base64 디코딩
121+
const decoded = decodeCustomBase64(token)
122+
if (decoded.length < 1 + 17) {
123+
throw new Error('디코딩 결과가 너무 짧습니다.')
124+
}
125+
126+
// 3. 길이와 헤더 검증
127+
const pathLen = decoded[0] // 원래 경로 길이 (mod 256)
128+
const headerBytes = decoded.slice(1, 1 + 17)
129+
const expectedHeader = deriveHeaderMask()
130+
for (let i = 0; i < 17; i++) {
131+
if (headerBytes[i] !== (expectedHeader[i] ^ pathLen)) {
132+
throw new Error('헤더 검증 실패: key/nonce가 잘못됐거나 토큰이 변조됨.')
133+
}
134+
}
135+
136+
// 4. 암호화된 경로 부분 복원 (RC4)
137+
const cipherPath = decoded.slice(1 + 17, 1 + 17 + pathLen)
138+
const rc4Key = deriveRc4Key()
139+
const plainPathBytes = rc4(rc4Key, cipherPath)
140+
141+
// 5. UTF-8 디코딩
142+
const decoder = new TextDecoder()
143+
const path = decoder.decode(plainPathBytes)
144+
145+
return path + queryPart // 필요 시 원래 쿼리도 복원
146+
}

sources/src/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { decodeIResult } from './decryptor.js'
2+
13
type unsafeWindow = typeof window
24
// eslint-disable-next-line @typescript-eslint/naming-convention
35
declare const unsafeWindow: unsafeWindow
@@ -82,4 +84,30 @@ Win.Function.prototype.bind = new Proxy(Win.Function.prototype.bind, {
8284
}
8385
return Reflect.apply(Target, ThisArg, Args)
8486
}
87+
})
88+
89+
Win.fetch = new Proxy(Win.fetch, {
90+
async apply(Target: typeof fetch, ThisArg: typeof Win, Args: Parameters<typeof fetch>) {
91+
let AwaitResult = Reflect.apply(Target, ThisArg, Args)
92+
let Result = await AwaitResult
93+
if (Result.headers.has('x-namuwiki-key') && Args[0] instanceof Request && Args[0].headers.has('x-namuwiki-nonce') &&
94+
decodeIResult(Args[0].url, Result.headers.get('x-namuwiki-key'), Args[0].headers.get('x-namuwiki-nonce'))) {
95+
return new Promise(() => {})
96+
}
97+
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && Args[1].headers instanceof Headers && Args[1].headers.has('x-namuwiki-nonce') &&
98+
decodeIResult(Args[0] instanceof URL ? Args[0].pathname : Args[0], Result.headers.get('x-namuwiki-key'), Args[1].headers.get('x-namuwiki-nonce'))) {
99+
return new Promise(() => {})
100+
}
101+
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && !(Args[1].headers instanceof Headers) &&
102+
Array.isArray(Args[1].headers) && Args[1].headers.some(InnerHeader => InnerHeader[0] === 'x-namuwiki-nonce') &&
103+
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])) {
104+
return new Promise(() => {})
105+
}
106+
if (Result.headers.has('x-namuwiki-key') && !(Args[0] instanceof Request) && !(Args[1].headers instanceof Headers) &&
107+
!Array.isArray(Args[1].headers) && typeof Args[1].headers['x-namuwiki-nonce'] === 'string' &&
108+
decodeIResult(Args[0] instanceof URL ? Args[0].pathname : Args[0], Result.headers.get('x-namuwiki-key'), Args[1].headers['x-namuwiki-nonce'])) {
109+
return new Promise(() => {})
110+
}
111+
return Result
112+
}
85113
})

0 commit comments

Comments
 (0)