kone base64 자동복호화

base64코드 자동복호화

// ==UserScript==
// @name         kone base64 자동복호화
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description   base64코드 자동복호화
// @author       SYJ
// @match        https://arca.live/*
// @match        https://kone.gg/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @require            https://openuserjs.org/src/libs/sizzle/GM_config.js
// @license MIT
// ==/UserScript==

// 자주 바뀜. 취약한 셀렉터
const SHADOW_ROOT_SELECTOR = "body  main div.prose-container";

const MAX_DECODE_COUNT = 10+1;

window.addEventListener('load', ()=>setTimeout(main, 1000));

async function main(){
    observeUrlChange(renderUI);
    const isAutoMode = await GM_getValue('toggleVal', true);
    if (isAutoMode) {
        observeUrlChange(applyAuto);
    }
    else {
        setTimeout(applyManually, 1000);
    }
}

function applyManually() {
    document.body.addEventListener('dblclick', function(e) {
        console.log('더블클릭 감지! 🎉',e.target,event.composedPath()[0]);
        const el = e.composedPath()[0];
        const nodes = Array.from(el.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)
        console.log(nodes)
        for (const node of nodes){
            const original = node.textContent;
            const decodedLink = doDecode(original);
            // console.log(node, original, decodedLink);
            if (original === decodedLink) continue;
            linkifyTextNode(node, decodedLink);
        }
    })
}

function applyAuto() {
    const contents = Array.from(document.body.querySelectorAll(`main ${textTagNames}`));
    const mainContents = Array.from(document.querySelector(SHADOW_ROOT_SELECTOR)?.shadowRoot?.querySelectorAll(textTagNames) ?? []);
    contents.push(...mainContents);

    for (const tag of contents) {
        const nodes = Array.from(tag.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)

        for (const node of nodes){
            const original = node.textContent;
            const decodedLink = doDecode(original);

            if (original === decodedLink) continue;
            linkifyTextNode(node, decodedLink);
        }
    }

    console.log('더이상 디코드할 수 없는 목록 :', nonBase64Collection);
}

const textTagNames = 'p, span, div, a, li,' +      // 일반 컨테이너
      'h1, h2, h3, h4, h5, h6,' +    // 제목 요소
      'em, strong, u, b, i, small, mark, ' +   // 인라인 포맷팅 요소
      'label, button, option, textarea' // 폼/인터페이스 요소

// 텍스트노드에 존재하는 url을 a태그로 바꿈. (텍스트노드 -> 텍스트노드1 + a태그 + 텍스트노드2)
function linkifyTextNode(Node, text) { // 텍스트노드 중 url을 찾아 a태그로 변환. (액션 포함)
    // URL 매칭 (https:// 로 시작해서 공백 전까지)
    const urlRegex = /(https?:\/\/[^\s]+)/;
    Node.textContent = text;

    if (!urlRegex.test(text)) { // URL 없으면 텍스트 덮어씌우고 종료
        return;
    }

    let node = Node;
    while(urlRegex.test(node?.textContent ?? '')){
        const match = urlRegex.exec(node.textContent);

        const url = match[0];
        const start = match.index;
        const urlLen = url.length;

        // "텍스트1 URL 텍스트2" 꼴의 텍스트노드를 세 개로 분리
        // 1) URL 앞부분과 뒤를 분리
        const textNode = document.createTextNode(node.textContent);
        const afterUrlStart = textNode.splitText(start);
        const afterUrlEnd = afterUrlStart.splitText(urlLen);
        const beforeUrlStart = textNode;

        // 3) <a> 요소 생성 후 URL 텍스트 노드 대신 교체. parent
        const a = makeATag(url)
        node.parentNode.replaceChild(a, node);
        node = afterUrlEnd;
        a.before(beforeUrlStart);
        a.after(afterUrlEnd);

    }

    function makeATag(link){
        const aTag = document.createElement('a');
        aTag.href = link;
        aTag.textContent = link;
        aTag.target = '_blank';
        aTag.rel = 'noreferrer';
        return aTag;
    }
}


function doDecode(text) {
    ///'use strict';
    let result = text;
    result = dec(/[0-9A-Za-z+/]{6,}[=]{0,2}/g, result); //문자열 6회 + '=' 0~2회
    return result;

    function dec(reg, text) {
        let result = text;
        const originals = Array.from(result.match(reg) ?? []);

        for (const original of originals){
            const decoded = decodeNtime(original);
            result = result.replace(original, decoded);
        }

        return result;
    }
}

// 노드 하나에 존재하는 모든 base64구문을 복원함.
function doDecode(text) {
    ///'use strict';
    let result = text;
    result = dec(/[0-9A-Za-z+/]{6,}[=]{0,2}/g, result); //문자열 6회 + '=' 0~2회
    return result;

    function dec(reg, text) {
        let result = text;
        const maps = Array.from(result.match(reg) ?? []) // base64 청크
        .map(o=>({before:o, after:decodeNtime(o)})) // base64 to 원본 매핑
        maps.forEach(({before, after})=>{result = result.replace(before, after)}); // 적용

        return result;
    }
}

// 원문으로 가능한 패턴 (한영숫자 + 자주쓰는 특문 + 한자)
// 허용 범위
// 한글                 : \uAC00-\uD7A3
// 히라가나             : \u3040-\u309F
// 카타카나             : \u30A0-\u30FF
// CJK 한자             : \u3400-\u4DBF, \u4E00-\u9FFF, \uF900-\uFAFF
// CJK 구두점·전각 특수문자: \u3000-\u303F
// 전각 괄호             : \uFF08-\uFF09
// 영숫자               : A-Za-z0-9
// 반각 특수문자         : !@#\$%\^&\*\(\)_\-\+=\[\]\{\}\\|;:'",.<>\/\?
const WORD_TEST = /^[ㄱ-ㅎ가-힣A-Za-z0-9!@#\$%\^&\*\(\)_\-\+=\[\]\{\}\\|;:/'",.<>\/\?!@#$%^&*()_+-=`~|\s\uAC00-\uD7A3\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF08-\uFF09]+$/;

const nonBase64Collection = [];

function decodeNtime(str) {
    let decoded = str;

    for (let i=0; i<MAX_DECODE_COUNT; i++){
        const old = decoded;
        decoded = decodeOneTime(decoded);
        if (decoded === old) return decoded;
    }

    function decodeOneTime(str) {
        try {
            const decoded = base64DecodeUnicode(str)
            if (!WORD_TEST.test(decoded)) {
                nonBase64Collection.push(str);
                throw new Error('[정상 유니코드 범위가 아님]'+JSON.stringify(str)+JSON.stringify(decoded));}
            return decoded;
        }
        catch(e) {
            //console.log('[FAIL]',str, e);
            return str; }
    }

    function base64DecodeUnicode0(str) {
        // 1) atob으로 디코딩 → 바이너리(한 글자당 1바이트) 문자열
        // 2) 각 문자 코드를 16진수 %xx 형태로 변환
        // 3) decodeURIComponent로 UTF-8 해석

        const percentEncodedStr = Array
        .from(atob(str))
        .map(char => '%' + char.charCodeAt(0).toString(16).padStart(2, '0'))
        .join('');
        return decodeURIComponent(percentEncodedStr);
    }
    function base64DecodeUnicode(str) {
        // 1) atob으로 디코딩 → 1바이트 문자열
        const binary = atob(str);

        // 2) 각 문자(=바이트)를 숫자로 뽑아 Uint8Array 생성
        const bytes = new Uint8Array(
            Array.from(binary, ch => ch.charCodeAt(0))
        );

        // 3) TextDecoder로 'utf-8' 디코딩
        return new TextDecoder('utf-8').decode(bytes);
    }

}

// UI

async function renderUI() {
    // 1) 값 로드
    let val = await GM_getValue('toggleVal', false);
    let menuId;

    // 2) 배지 생성
    /* const badge = document.createElement('div');
    Object.assign(badge.style, {
        position: 'fixed',
        top: '10px',
        right: '10px',
        padding: '4px 8px',
        background: 'rgba(0,0,0,0.7)',
        color: '#fff',
        fontSize: '14px',
        borderRadius: '4px',
        zIndex: '9999',
    });
    document.body.append(badge);
    */

    // 3) 렌더 함수
    function render() {
        // 메뉴 해제 후 다시 등록
        if (menuId) GM_unregisterMenuCommand(menuId);
        menuId = GM_registerMenuCommand(
            `자동모드 토글 (현재: ${val?'ON':'OFF'})`,
            toggleValue
        );
        // 배지 업데이트
        //badge.textContent = `현재 값: ${val}`;
    }

    // 4) 토글 함수 (즉시 UI 업데이트 포함)
    async function toggleValue() {
        const newVal = !val;
        await GM_setValue('toggleVal', newVal);
        val = newVal;    // 변수 갱신
        render();        // 메뉴·배지 즉시 갱신
    }

    // 초기 렌더
    render();
}

const observeUrlChange = (func) => {
    func();

    let oldHref = document.location.href;
    const body = document.querySelector('body');
    const observer = new MutationObserver(mutations => {
        if (oldHref !== document.location.href) {
            oldHref = document.location.href;
            setTimeout(func, 1000);

        }
    });
    observer.observe(body, { childList: true, subtree: true });
};