kone base64 자동복호화

base64코드 자동복호화

As of 2025-06-15. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         kone base64 자동복호화
// @namespace    http://tampermonkey.net/
// @version      1.4.2
// @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 .contents ${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('더이상 디코드할 수 없는 목록 :', Array.from(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태그로 바꿈. (url포함 텍스트노드 -> 텍스트노드1 + a태그 + 텍스트노드2)
function linkifyTextNode(Node, text) {
    const urlRegex = /(https?:\/\/[^\s]+)/; // URL 매칭 (https:// 로 시작해서 공백 전까지)
    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;

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



// 노드 하나에 존재하는 모든 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 = new Set();

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.add(str);
                throw new Error('[정상 유니코드 범위가 아님]'+JSON.stringify(str)+JSON.stringify(decoded));}
            return decoded;
        }
        catch(e) {
            //console.log('[FAIL]',str, e);
            return str; }
    }

    function base64DecodeUnicode(str) {
        const binary = atob(str); // 1) atob으로 디코딩 → 1바이트 문자열
        const bytes = new Uint8Array(
            Array.from(binary, ch => ch.charCodeAt(0)) // 2) 각 문자(=바이트)를 숫자로 뽑아 Uint8Array 생성
        );
        return new TextDecoder('utf-8').decode(bytes); // 3) TextDecoder로 'utf-8' 디코딩
    }

}

// UI

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

    // 렌더
    render();

    function render() {
        // 메뉴 해제 후 다시 등록
        if (menuId) GM_unregisterMenuCommand(menuId);
        menuId = GM_registerMenuCommand(
            `자동모드 토글 (현재: ${val?'ON':'OFF'})`,
            toggleValue
        );
    }

    async function toggleValue() {
        const newVal = !val;
        await GM_setValue('toggleVal', newVal);
        val = newVal;    // 변수 갱신
        render();        // 메뉴·배지 즉시 갱신
    }
}

const observeUrlChange = (func) => {
    func();
    const body = document.querySelector('body');
    const throttled = throttle(func, 200);
    const observer = new MutationObserver(mutations => {
         throttled();
         setTimeout(throttled, 500);
         setTimeout(throttled, 1000);
    });
    observer.observe(body, { childList: true, subtree: true, characterData:true });

};

function throttle(func, delay) {
let timeoutId = null;
  return function() {
    if (!timeoutId) {
      timeoutId = setTimeout(() => {
        func();
        timeoutId = null;
      }, delay);
    }
  };
}