Ephemeral Encryptor

Encrypts/decrypts text using session-based password & markers. Provides input area for encryption.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Ephemeral Encryptor
// @namespace    https://greasyfork.org/en/users/1465519-user89109
// @version      1.0
// @description  Encrypts/decrypts text using session-based password & markers. Provides input area for encryption.
// @author       UwU7.com
// @license      All Rights Reserved
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants and Defaults ---
    const DEFAULT_PASSWORD = "OMkxyLK8tjuvdxbQwG7A";
    const DEFAULT_START_MARKER = "[UwU7]";
    const DEFAULT_END_MARKER = "[/UwU7]";
    const SCRIPT_NAME = "Ephemeral Encryptor";

    // --- Session State Variables ---
    let currentPassword = DEFAULT_PASSWORD;
    let currentStartMarker = DEFAULT_START_MARKER;
    let currentEndMarker = DEFAULT_END_MARKER;

    // --- Crypto Helper Functions ---
    function arrayBufferToBase64(buffer) {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        const len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary);
    }

    function base64ToArrayBuffer(base64) {
        const binary_string = window.atob(base64);
        const len = binary_string.length;
        const bytes = new Uint8Array(len);
        for (let i = 0; i < len; i++) {
            bytes[i] = binary_string.charCodeAt(i);
        }
        return bytes.buffer;
    }

    async function getKey(password, salt) {
        const enc = new TextEncoder();
        const keyMaterial = await window.crypto.subtle.importKey(
            "raw",
            enc.encode(password),
            { name: "PBKDF2" },
            false,
            ["deriveKey"]
        );
        return await window.crypto.subtle.deriveKey(
            {
                name: "PBKDF2",
                salt: salt,
                iterations: 100000,
                hash: "SHA-256"
            },
            keyMaterial,
            { name: "AES-GCM", length: 256 },
            true,
            ["encrypt", "decrypt"]
        );
    }

    async function encryptText(plainText, password) {
        try {
            const salt = window.crypto.getRandomValues(new Uint8Array(16));
            const iv = window.crypto.getRandomValues(new Uint8Array(12));
            const key = await getKey(password, salt);
            const enc = new TextEncoder();
            const encodedText = enc.encode(plainText);
            const ciphertext = await window.crypto.subtle.encrypt(
                { name: "AES-GCM", iv: iv },
                key,
                encodedText
            );
            return JSON.stringify({
                s: arrayBufferToBase64(salt),
                i: arrayBufferToBase64(iv),
                c: arrayBufferToBase64(ciphertext)
            });
        } catch (error) {
            console.error(`${SCRIPT_NAME} Encryption Error:`, error);
            throw error;
        }
    }

    async function decryptText(encryptedJsonString, password) {
        try {
            const encryptedData = JSON.parse(encryptedJsonString);
            if (!encryptedData.s || !encryptedData.i || !encryptedData.c) {
                throw new Error("Invalid encrypted data format.");
            }
            const salt = base64ToArrayBuffer(encryptedData.s);
            const iv = base64ToArrayBuffer(encryptedData.i);
            const ciphertext = base64ToArrayBuffer(encryptedData.c);
            const key = await getKey(password, salt);
            const decryptedBuffer = await window.crypto.subtle.decrypt(
                { name: "AES-GCM", iv: iv },
                key,
                ciphertext
            );
            const dec = new TextDecoder();
            return dec.decode(decryptedBuffer);
        } catch (error) {
            throw error;
        }
    }

    // --- UI Functions ---

    /**
     * Creates and displays a modal for text input and encryption.
     */
    function showEncryptionInputModal() {
        const modalId = 'ephemeral-encryptor-input-modal';
        const existingModal = document.getElementById(modalId);
        if (existingModal) {
            existingModal.remove();
        }

        const modalOverlay = document.createElement('div');
        modalOverlay.id = modalId;
        modalOverlay.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(0,0,0,0.5); display: flex;
            align-items: center; justify-content: center; z-index: 2147483646;
        `;

        const modalContent = document.createElement('div');
        modalContent.style.cssText = `
            background-color: #fff; padding: 25px; border-radius: 8px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.2); width: 90%; max-width: 600px;
            display: flex; flex-direction: column; gap: 15px;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        `;

        const title = document.createElement('h3');
        title.textContent = 'Encrypt Text';
        title.style.cssText = "margin: 0 0 5px 0; text-align: center; font-size: 1.4em; color: #333;";

        const textarea = document.createElement('textarea');
        textarea.rows = 10;
        textarea.placeholder = "Enter text to encrypt here...";
        textarea.style.cssText = `
            width: calc(100% - 20px); padding: 10px; border-radius: 5px;
            border: 1px solid #ccc; font-size: 1em; resize: vertical;
            min-height: 100px;
        `;

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = "display: flex; justify-content: space-between; gap: 10px;";

        const encryptButton = document.createElement('button');
        encryptButton.textContent = 'Encrypt & Copy to Clipboard';
        encryptButton.style.cssText = `
            padding: 10px 15px; background-color: #007bff; color: white;
            border: none; border-radius: 5px; cursor: pointer; font-size: 1em; flex-grow: 1;
            transition: background-color 0.2s;
        `;
        encryptButton.onmouseover = () => encryptButton.style.backgroundColor = '#0056b3';
        encryptButton.onmouseout = () => encryptButton.style.backgroundColor = '#007bff';

        const closeButton = document.createElement('button');
        closeButton.textContent = 'Close';
        closeButton.style.cssText = `
            padding: 10px 15px; background-color: #6c757d; color: white;
            border: none; border-radius: 5px; cursor: pointer; font-size: 1em;
            transition: background-color 0.2s;
        `;
        closeButton.onmouseover = () => closeButton.style.backgroundColor = '#545b62';
        closeButton.onmouseout = () => closeButton.style.backgroundColor = '#6c757d';

        encryptButton.addEventListener('click', async () => {
            const plainText = textarea.value;
            if (plainText.trim() === "") {
                alert("Textarea is empty. Nothing to encrypt.");
                return;
            }
            try {
                const encryptedPayload = await encryptText(plainText, currentPassword);
                const fullEncryptedBlock = currentStartMarker + encryptedPayload + currentEndMarker;
                GM_setClipboard(fullEncryptedBlock);
                encryptButton.textContent = 'Copied!';
                textarea.value = ''; // Clear textarea after copying
                setTimeout(() => {
                    encryptButton.textContent = 'Encrypt & Copy to Clipboard';
                }, 2000);
            } catch (error) {
                alert("Encryption failed. See console for details.");
                console.error(`${SCRIPT_NAME} Modal Encryption Error:`, error);
            }
        });

        closeButton.addEventListener('click', () => {
            modalOverlay.remove();
        });

        const escapeKeyListener = (event) => {
            if (event.key === "Escape") {
                modalOverlay.remove();
                document.removeEventListener('keydown', escapeKeyListener);
            }
        };
        document.addEventListener('keydown', escapeKeyListener);
        
        modalContent.addEventListener('click', (event) => event.stopPropagation());
        modalOverlay.addEventListener('click', () => {
            modalOverlay.remove();
            document.removeEventListener('keydown', escapeKeyListener);
        });

        buttonContainer.appendChild(encryptButton);
        buttonContainer.appendChild(closeButton);

        modalContent.appendChild(title);
        modalContent.appendChild(textarea);
        modalContent.appendChild(buttonContainer);
        modalOverlay.appendChild(modalContent);
        document.body.appendChild(modalOverlay);
        textarea.focus();
    }

    // --- Core Logic for Finding and Processing Encrypted Blocks ---
    async function processDecryptionInTextNode(node) {
        let nodeChanged = false;
        let content = node.nodeValue;
        let searchStartIndex = 0;

        while (searchStartIndex < content.length) {
            const startIndexFull = content.indexOf(currentStartMarker, searchStartIndex);
            if (startIndexFull === -1) break;

            const endIndexFull = content.indexOf(currentEndMarker, startIndexFull + currentStartMarker.length);
            if (endIndexFull === -1) break;

            const payloadStartIndex = startIndexFull + currentStartMarker.length;
            const potentialPayload = content.substring(payloadStartIndex, endIndexFull);

            if (potentialPayload.trim() === "") {
                searchStartIndex = endIndexFull + currentEndMarker.length;
                continue;
            }

            try {
                const decryptedText = await decryptText(potentialPayload, currentPassword);
                const before = content.substring(0, startIndexFull);
                const after = content.substring(endIndexFull + currentEndMarker.length);
                content = before + decryptedText + after;
                nodeChanged = true;
                searchStartIndex = (before + decryptedText).length;
            } catch (error) {
                searchStartIndex = endIndexFull + currentEndMarker.length;
            }
        }

        if (nodeChanged) {
            node.nodeValue = content;
        }
        return nodeChanged;
    }

    async function scanAndDecrypt(rootElement = document.body) {
        const textNodesToProcess = [];
        const treeWalker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, {
            acceptNode: function (node) {
                if (node.parentElement && (node.parentElement.closest('script, style, textarea, input, [contenteditable="true"], #ephemeral-encryptor-input-modal'))) {
                    return NodeFilter.FILTER_REJECT;
                }
                if (!node.nodeValue || node.nodeValue.trim() === '') {
                    return NodeFilter.FILTER_REJECT;
                }
                if (node.nodeValue.includes(currentStartMarker) && node.nodeValue.includes(currentEndMarker)) {
                    return NodeFilter.FILTER_ACCEPT;
                }
                return NodeFilter.FILTER_REJECT;
            }
        });

        let currentNode;
        while (currentNode = treeWalker.nextNode()) {
            textNodesToProcess.push(currentNode);
        }

        for (const textNode of textNodesToProcess) {
           await processDecryptionInTextNode(textNode);
        }
    }

    // encryptAndReplaceSelection function is removed as per user request.

    // --- MutationObserver for Dynamic Content ---
    const observer = new MutationObserver(mutationsList => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(newNode => {
                    if (newNode.nodeType === Node.ELEMENT_NODE) {
                        if (newNode.id !== 'ephemeral-encryptor-input-modal' && !newNode.closest('#ephemeral-encryptor-input-modal')) {
                            scanAndDecrypt(newNode);
                        }
                    }
                });
            } else if (mutation.type === 'characterData') {
                 if (mutation.target && mutation.target.nodeType === Node.TEXT_NODE) {
                    const parent = mutation.target.parentElement;
                    if (parent && parent.id !== 'ephemeral-encryptor-input-modal' && !parent.closest('#ephemeral-encryptor-input-modal')) {
                        if (mutation.target.nodeValue.includes(currentStartMarker) && mutation.target.nodeValue.includes(currentEndMarker)) {
                            processDecryptionInTextNode(mutation.target);
                        }
                    }
                 }
            }
        }
    });

    // --- Violentmonkey Menu Commands ---
    GM_registerMenuCommand('Set Session Password', () => {
        const newPassword = prompt(`${SCRIPT_NAME}: Enter session password:`, "");
        if (newPassword !== null) {
            currentPassword = newPassword || DEFAULT_PASSWORD;
            alert(`${SCRIPT_NAME}: Session password ${currentPassword === DEFAULT_PASSWORD ? 'reverted to default' : 'updated'}.`);
            scanAndDecrypt();
        }
    });

    GM_registerMenuCommand('Set Session Markers', () => {
        const newStartMarker = prompt(`${SCRIPT_NAME}: Enter START marker:`, currentStartMarker);
        if (newStartMarker === null) return;

        const newEndMarker = prompt(`${SCRIPT_NAME}: Enter END marker:`, currentEndMarker);
        if (newEndMarker === null) return;

        currentStartMarker = newStartMarker.trim() || DEFAULT_START_MARKER;
        currentEndMarker = newEndMarker.trim() || DEFAULT_END_MARKER;
        alert(`${SCRIPT_NAME}: Session markers updated.`);
        scanAndDecrypt();
    });

    GM_registerMenuCommand('View Current Settings', () => {
        alert(
            `${SCRIPT_NAME} - Session Settings:\n\n` +
            `Password Type: ${currentPassword === DEFAULT_PASSWORD ? 'Default' : 'Custom'}\n` +
            `Start Marker: ${currentStartMarker}\n` +
            `End Marker: ${currentEndMarker}\n\n` +
            `(Settings are for the current session only.)`
        );
    });

    // GM_registerMenuCommand('Encrypt Selection (on-page)', encryptAndReplaceSelection); // Removed
    GM_registerMenuCommand('Encrypt Text (Input & Copy)', showEncryptionInputModal);

    GM_registerMenuCommand('Manually Re-scan Page', () => {
        scanAndDecrypt();
        alert(`${SCRIPT_NAME}: Page re-scan complete.`);
    });


    // --- Initialization ---
    function initialize() {
        scanAndDecrypt();
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
        console.log(`${SCRIPT_NAME} initialized. Using ${currentPassword === DEFAULT_PASSWORD ? 'default' : 'custom session'} password.`);
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        initialize();
    } else {
        document.addEventListener('DOMContentLoaded', initialize, { once: true });
    }

})();