kone base64 자동복호화

base64코드 자동복호화

目前為 2025-05-29 提交的版本,檢視 最新版本

// ==UserScript==
// @name         kone base64 자동복호화
// @namespace    http://tampermonkey.net/
// @version      1.2.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";

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

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

function doDecode(text) {
    ///'use strict';
    let result = text;
    //result = dec(/aHR0c[0-9A-Za-z+]{20,}[=]{0,2}/, result); //aHR0c:1회인코딩된것.
    //result = dec(/YUhSMGN[0-9A-Za-z]{20,}[=]{0,2}/, result, true); //YUhSMGN:2회인코딩된것.
    //result = dec(/WVVoU01HTklUVFp[0-9A-Za-z+]{20,}[=]{0,2}/, result, true); // 3회인코딩된것.
    //result = dec(/V1ZWb1UwMUhUa2[0-9A-Za-z]{20,}[=]{0,2}/, result, true); // 4회인코딩된것.
    result = dec(/[0-9A-Za-z]{6,}[=]{0,2}/, result); //문자열 5회 + '=' 1~2회
    //result = dec(/[0-9A-Za-z]{50,}[=]{0,2}/, result); //문자열 200회 + '=' 0~2회
    return result;
}

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

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

const MAX_DECODE_COUNT = 10+1;

function decodeNtime(str) {
    let decoded = str;
    let old = str;
    for (let i=0; i<MAX_DECODE_COUNT; i++){
        old = decoded;
        decoded = decodeOneTime(decoded);
        console.log(decoded, old)
        if (decoded === old) return decoded;
    }

    function decodeOneTime(str) {
        try{ return base64DecodeUnicode(str);}
        catch{ return str;}
    }

    function base64DecodeUnicode(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 manuallyApply() {
    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 autoApply() {
    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;
            console.log('[DECODE] ',original, decodedLink);
            linkifyTextNode(node, decodedLink);
        }
    }
}

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

function linkifyTextNode(Node, text) { // 텍스트노드 중 url을 찾아 a태그로 변환. (액션 포함)
    // URL 매칭 (https:// 로 시작해서 공백 전까지)
    const urlRegex = /(https?:\/\/[^\s]+)/;
    const match = urlRegex.exec(text);
    if (!match) { // URL 없으면 텍스트 덮어씌우고 종료
        Node.textContent = text;
        return;
    }

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

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

    // 3) <a> 요소 생성 후 URL 텍스트 노드 대신 교체
    const a = makeATag(url)
    Node.parentNode.replaceChild(a, Node);
    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;
    }
}


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