|
| 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 | +} |
0 commit comments