Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Secret Attachment (b4k)
- // @namespace http://tampermonkey.net/
- // @version 2.0
- // @description Detect and decode secret attachments hosted on catbox/fatbox from both OP and reply posts on arch.b4k.dev
- // @author anon
- // @match https://arch.b4k.dev/*
- // @grant GM_xmlhttpRequest
- // @connect catbox.moe
- // @connect files.catbox.moe
- // @connect fatbox.moe
- // @connect files.fatbox.moe
- // ==/UserScript==
- /* global BigInt */
- (async function() {
- "use strict";
- const DEBUG = false; // set true if you want extra console logs
- // --- compatibility: add Blob/Request/Response.bytes() ---
- async function bytes() {
- return new Uint8Array(await this.arrayBuffer());
- }
- for (const C of ['Blob', 'Request', 'Response']) {
- const p = globalThis[C]?.prototype;
- if (p && !p.bytes) p.bytes = bytes;
- }
- // --- GM_fetch wrapper using GM_xmlhttpRequest ---
- function GM_fetch(url, opt = {}) {
- function parseHeaders(headerStr) {
- const headers = new Headers()
- if (!headerStr) return headers;
- headerStr.trim().split(/[\r\n]+/).forEach((line) => {
- const i = line.indexOf(':');
- if (i === -1) return;
- const key = line.slice(0, i).trim();
- const value = line.slice(i + 1).trim();
- if (key) headers.append(key, value)
- })
- return headers
- }
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- url,
- method: opt.method || "GET",
- data: opt.body,
- responseType: "blob",
- onload: res => {
- resolve(new Response(res.response, {
- status: res.status,
- headers: parseHeaders(res.responseHeaders)
- }))
- },
- ontimeout: () => reject(new Error("Request timed out")),
- onabort: () => reject(new Error("Request aborted")),
- onerror: () => reject(new Error("Network error. Catbox might be blocking your connection.")),
- onprogress: (event) => {
- if (opt.onProgress && event.lengthComputable) {
- const percent = Math.round((event.loaded / event.total) * 100);
- opt.onProgress(percent, event.loaded, event.total);
- }
- }
- })
- })
- }
- // --- tiny DOM helpers ---
- function $(tag, ...args) {
- let attributes = {}, children = []
- if (args.length === 1) {
- if (Array.isArray(args[0])) children = args[0]
- else attributes = args[0]
- } else if (args.length === 2) {
- attributes = args[0]
- children = args[1]
- }
- const element = document.createElement(tag)
- for (const [name, value] of Object.entries(attributes)) {
- if (typeof value === "boolean") {
- if (value) element.setAttribute(name, "")
- } else element.setAttribute(name, value)
- }
- if (children && children.length) element.append(...children)
- return element
- }
- function insertAfter(newNode, referenceNode) {
- if (!referenceNode || !referenceNode.parentNode) return;
- return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling)
- }
- function bigIntFromBytes(bytes) {
- if (!bytes || bytes.length === 0) return 0n;
- let n = BigInt(bytes[0])
- for (let i = 1; i < bytes.length; ++i) n |= BigInt(bytes[i]) << (BigInt(i) << 3n)
- return n
- }
- function encode(input, alphabet, minDigits = 1) {
- const length = BigInt(alphabet.length)
- let n = bigIntFromBytes(input)
- let result = ""
- while (n > 0n || result.length < minDigits) {
- result = alphabet[Number(n % length)] + result
- n /= length
- }
- return result
- }
- function decode(input, alphabet) {
- const length = BigInt(alphabet.length)
- let n = 0n
- for (let i = 0; i < input.length; i++) n = n * length + BigInt(alphabet.indexOf(input[i]))
- const byteCount = n === 0n ? 1 : (n.toString(2).length + 7) >>> 3
- const bytes = new Uint8Array(byteCount)
- for (let i = 0; i < byteCount; i++) {
- bytes[i] = Number(n & 0xFFn)
- n = n >> 8n
- }
- return bytes
- }
- async function sha256(input, length = 32) {
- const digest = await crypto.subtle.digest("sha-256", input);
- return new Uint8Array(digest, 0, length);
- }
- function arraysEqual(arr1, arr2) {
- if (!arr1 || !arr2) return false;
- if (arr1.length !== arr2.length) return false
- for (let i = 0; i < arr1.length; ++i) if (arr1[i] !== arr2[i]) return false
- return true
- }
- function basename(input) {
- if (!input) return input;
- try {
- return input.split("/").pop().split("?")[0].split("#")[0];
- } catch (e) { return input; }
- }
- function parse(filename) {
- if (!filename) return { name: "", ext: "" }
- const lastIndex = filename.lastIndexOf(".")
- return lastIndex === -1
- ? { name: filename, ext: "" }
- : { name: filename.slice(0, lastIndex), ext: filename.slice(lastIndex + 1) }
- }
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
- async function embed(key, name) {
- const data = new Uint8Array([...key, ...new TextEncoder().encode(name)])
- const hash = await sha256(data, 4)
- return encode(new Uint8Array([...data, ...hash]), alphabet)
- }
- async function extract(input) {
- const name = parse(input).name
- if (/^[A-Za-z0-9]+$/.test(name)) {
- const bytes = decode(name, alphabet)
- if (!bytes || bytes.length <= 4) return null;
- const [data, hash] = [bytes.slice(0, -4), bytes.slice(-4)]
- if (arraysEqual(await sha256(data, 4), hash)) {
- return { key: data.slice(0, 16), name: new TextDecoder().decode(data.slice(16)) }
- }
- }
- return null
- }
- async function encrypt(data, key) {
- const cryptoKey = await crypto.subtle.importKey(
- "raw",
- key,
- { name: "AES-CTR" },
- false,
- ["encrypt"]
- )
- const cipher = await crypto.subtle.encrypt(
- { name: "AES-CTR", counter: new Uint8Array(16), length: 64 },
- cryptoKey,
- data
- )
- return new Blob([cipher])
- }
- async function uploadFile(file, key) {
- const data = new Uint8Array(await file.arrayBuffer())
- const encryptedFile = await encrypt(data, key)
- const form = new FormData()
- form.set("reqtype", "fileupload")
- form.set("fileToUpload", encryptedFile, file.name)
- return GM_fetch("https://catbox.moe/user/api.php", { method: "POST", body: form }).then(res => res.text())
- }
- async function downloadFile(file, key, progressCallback) {
- const fatboxLink = `https://files.fatbox.moe/${file}`;
- const catboxLink = `https://files.catbox.moe/${file}`;
- let response;
- try {
- response = await GM_fetch(fatboxLink, { onProgress: progressCallback });
- if (response.status < 200 || response.status >= 500) response = await GM_fetch(catboxLink, { onProgress: progressCallback });
- } catch (e) {
- response = await GM_fetch(catboxLink, { onProgress: progressCallback });
- }
- if (response.status < 200 || response.status >= 500) throw new Error(`Unexpected response status code ${response.status}`);
- return await response.blob();
- }
- function mime(ext) {
- return ({
- "mp4": "video/mp4",
- "webm": "video/webm",
- "ogg": "video/ogg",
- "avi": "video/x-msvideo",
- "ts": "video/mp2t",
- "3gp": "video/3gpp",
- "3g2": "video/3gpp2",
- "aac": "audio/aac",
- "mp3": "audio/mpeg",
- "wav": "audio/wav",
- "weba": "audio/webm",
- "png": "image/png",
- "jpg": "image/jpeg",
- "jpeg": "image/jpeg",
- "gif": "image/gif",
- "bmp": "image/bmp",
- "webp": "image/webp"
- })[ext]
- }
- // --- Helper to extract filename robustly from a file link ---
- function getFilenameFromLink(fileLink) {
- if (!fileLink) return "";
- let v = fileLink.getAttribute('title');
- if (v && v.trim()) return v.trim();
- v = fileLink.getAttribute('download');
- if (v && v.trim()) return v.trim();
- const tc = (fileLink.textContent || "").trim();
- if (tc) return tc;
- const href = fileLink.getAttribute('href');
- if (href) return basename(href);
- return "";
- }
- // --- Process secret attachments in posts ---
- async function processSecretAttachments(shouldNotify = true) {
- // SELECT ALL filename links (OP and replies). don't require [title].
- const fileElements = Array.from(document.querySelectorAll(".post_file_filename, .op_file_filename"));
- if (DEBUG) console.debug("SecretAttachment: scanning fileElements", fileElements.length);
- let secretsFound = 0;
- let newSecrets = 0;
- for (const fileLink of fileElements) {
- if (!fileLink) continue;
- const filenameAttr = getFilenameFromLink(fileLink);
- if (!filenameAttr) {
- if (DEBUG) console.debug("SecretAttachment: no filename from link", fileLink);
- continue;
- }
- // find the containing post element (article or .post_wrapper)
- const postContainer = fileLink.closest('article, .post_wrapper, .post_is_op');
- if (!postContainer) {
- if (DEBUG) console.debug("SecretAttachment: no post container for", filenameAttr);
- continue;
- }
- if (postContainer.dataset.secretProcessed === 'true') {
- if (DEBUG) console.debug("SecretAttachment: already processed", filenameAttr);
- continue;
- }
- const postMessageElement = postContainer.querySelector('.text');
- if (!postMessageElement) {
- if (DEBUG) console.debug("SecretAttachment: no .text element in post", postContainer);
- continue;
- }
- // Use the filename base (strip path/params)
- const parsedFilenameRaw = basename(filenameAttr).trim();
- const parsedFilename = parse(parsedFilenameRaw).name;
- if (DEBUG) console.debug("SecretAttachment: parsedFilenameRaw", parsedFilenameRaw, "->", parsedFilename);
- const result = await extract(parsedFilename);
- if (!result) {
- if (DEBUG) console.debug("SecretAttachment: not a secret filename:", parsedFilename);
- continue;
- }
- // Found a secret
- secretsFound++;
- newSecrets++;
- postContainer.dataset.secretProcessed = 'true';
- postContainer.classList.add('has-secret-attachment');
- const { key, name } = result;
- const secretLink = $("a", { href: "javascript:void(0)" }, [`secret.${parse(name).ext}`]);
- const filename = secretLink.textContent;
- const secret = $("div", { class: "secretFile", style: "margin-top: 8px; margin-bottom: 4px;" }, ["[", secretLink, "]"]);
- secretLink.addEventListener("click", async function() {
- secretLink.textContent = 'Loading ' + filename + '... (0%)';
- try {
- let lastPercent = 0;
- const encryptedBlob = await downloadFile(name, key, (percent) => {
- if (percent !== lastPercent) {
- lastPercent = percent;
- secretLink.textContent = `Loading ${filename}... (${percent}%)`;
- }
- });
- // --- Decrypt the blob ---
- const encryptedBytes = new Uint8Array(await encryptedBlob.arrayBuffer());
- const cryptoKey = await crypto.subtle.importKey(
- "raw",
- key,
- { name: "AES-CTR" },
- false,
- ["decrypt"]
- );
- const decryptedBuffer = await crypto.subtle.decrypt(
- { name: "AES-CTR", counter: new Uint8Array(16), length: 64 },
- cryptoKey,
- encryptedBytes
- );
- const type = mime(parse(name).ext) || '';
- function arrayBufferToDataURL(buffer, type) {
- let binary = '';
- const bytes = new Uint8Array(buffer);
- const chunkSize = 0x8000;
- for (let i = 0; i < bytes.length; i += chunkSize) {
- const sub = bytes.subarray(i, i + chunkSize);
- binary += String.fromCharCode.apply(null, sub);
- }
- return `data:${type};base64,${btoa(binary)}`;
- }
- if (type.startsWith("video") || type.startsWith("audio") || type.startsWith("image")) {
- const dataURL = arrayBufferToDataURL(decryptedBuffer, type);
- const elementTag = type.startsWith("video") ? "video" :
- type.startsWith("audio") ? "audio" : "img";
- secret.replaceWith($(elementTag, {
- src: dataURL,
- controls: type.startsWith("video") || type.startsWith("audio") ? true : undefined,
- style: "max-width: 100%;"
- }));
- } else {
- const dataURL = arrayBufferToDataURL(decryptedBuffer, type || 'application/octet-stream');
- const a = $("a", { href: dataURL });
- a.download = `${crypto.randomUUID()}.${parse(name).ext}`;
- document.body.appendChild(a);
- a.click();
- a.remove();
- }
- } catch (error) {
- console.error({script: 'SecretAttachment (b4k)', error});
- secretLink.textContent = `Error loading ${filename}. Error logged to console.${error && error.message ? ' ' + error.message : ''}`;
- }
- });
- insertAfter(secret, postMessageElement);
- }
- if (newSecrets > 0 && shouldNotify) showSecretFoundNotification(secretsFound);
- if (DEBUG) console.debug("SecretAttachment: finished scan. secretsFound:", secretsFound);
- return secretsFound;
- }
- // --- Styles ---
- function addSecretHighlightStyles() {
- const style = document.createElement('style');
- style.textContent = `
- @keyframes secretPulse { 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.2); } 70% { box-shadow: 0 0 0 8px rgba(255,255,255,0); } 100% { box-shadow:0 0 0 0 rgba(255,255,255,0); } }
- .post_wrapper.has-secret-attachment, .post_is_op.has-secret-attachment, article.has-secret-attachment { position:relative !important; border:2px solid #555 !important; border-radius:4px !important; box-shadow:0 0 6px rgba(255,255,255,0.2) !important; animation: secretPulse 2s ease-out; animation-iteration-count:1; padding:2px !important; margin-bottom:6px !important; }
- .has-secret-attachment::before { content:"🔒"; position:absolute; top:2px; right:2px; font-size:14px; opacity:0.8; z-index:100; }
- .secretFile { display:inline-block; padding:2px 5px; background-color:rgba(0,0,0,0.2); border-radius:3px; margin-top:8px !important; margin-bottom:4px !important;}
- .secretFile a { color:#fff !important; text-decoration:none !important; }
- .secretFile a:hover { text-decoration:underline !important; }
- #secret-notification { font-family: arial, sans-serif; }
- `;
- document.head.appendChild(style);
- }
- // --- notification ---
- function showSecretFoundNotification(count) {
- const existingNotification = document.getElementById('secret-notification');
- if (existingNotification) existingNotification.remove();
- const notification = $("div", { id: "secret-notification", style: "position:fixed; top:calc(3vh + 30px); right:20px; background-color:#000; border:1px solid #333; border-radius:4px; padding:10px 15px; box-shadow:0 2px 10px rgba(0,0,0,0.3); z-index:9999; font-family:arial,sans-serif; font-size:14px; color:#fff; max-width:300px; animation:fadeIn 0.3s ease-out;" }, [
- $("div", { style:"display:flex; align-items:center; margin-bottom:5px;" }, [
- $("span", { style:"font-weight:bold; margin-right:auto; color:#fff;" }, ["Secret Attachment"]),
- $("span", { id:"secret-notification-close", style:"cursor:pointer; font-size:18px; line-height:14px; color:#999;" }, ["×"])
- ]),
- $("div", {}, [ count===1?"1 secret attachment detected on this page.":`${count} secret attachments detected on this page.` ]),
- $("style", {}, [`@keyframes fadeIn { from { opacity:0; transform:translateY(-10px);} to { opacity:1; transform:translateY(0);}} @keyframes fadeOut { from{opacity:1; transform:translateY(0);} to{opacity:0; transform:translateY(-10px);} } #secret-notification-close:hover { color:#fff; }`])
- ]);
- document.body.appendChild(notification);
- document.getElementById('secret-notification-close').addEventListener('click', () => notification.remove());
- setTimeout(() => {
- if (notification.parentNode) {
- notification.style.animation='fadeOut 0.3s ease-in forwards';
- notification.addEventListener('animationend', () => { if(notification.parentNode) notification.remove(); });
- }
- }, 7000);
- }
- // --- initial boot ---
- addSecretHighlightStyles();
- await processSecretAttachments(true);
- // --- observe DOM ---
- const observer = new MutationObserver((mutations) => {
- let shouldProcess = false;
- for (const mutation of mutations) {
- if (mutation.type === 'childList') {
- for (const node of mutation.addedNodes) {
- if (node.nodeType !== 1) continue;
- try {
- if (node.matches && (node.matches('.post_wrapper') || node.matches('article') || node.matches('.thread') || node.matches('.post_file') )) {
- shouldProcess = true; break;
- }
- } catch (e) {}
- if (node.querySelector && (node.querySelector('.post_file_filename') || node.querySelector('.op_file_filename') || node.querySelector('.post_file'))) {
- shouldProcess = true; break;
- }
- }
- }
- if (shouldProcess) break;
- }
- if (shouldProcess) {
- // small delay to allow inner nodes to render
- setTimeout(()=> processSecretAttachments(false), 150);
- }
- });
- observer.observe(document.body, { childList:true, subtree:true });
- if (DEBUG) console.debug("SecretAttachment: initialized");
- })();
Add Comment
Please, Sign In to add comment