jQuery(function() {
const CDN_URL = 'https://cdn.jsdelivr.net/npm/openpgp@5.11.0/dist/openpgp.min.js';
/**
* Aggressive Sanitizer: This fixes the "Error during parsing" by
* reconstructing the PGP Armor block from scratch, ensuring the
* mandatory blank line after headers is present.
*/
function sanitizeArmor(text) {
const lines = text.trim().split(/\r?\n/);
let headerLine = "";
let tailLine = "";
let headers = [];
let body = [];
let checksum = "";
let section = "START";
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (!line && section !== "BODY") continue;
if (line.startsWith('-----BEGIN PGP')) {
headerLine = line;
section = "HEADER";
continue;
}
if (line.startsWith('-----END PGP')) {
tailLine = line;
section = "END";
continue;
}
if (section === "HEADER") {
// If it looks like a header (contains ': '), keep it.
if (line.includes(': ')) {
headers.push(line);
} else if (/^[A-Za-z0-9+/]/.test(line)) {
// If it looks like Base64, the body started early (missing blank line)
section = "BODY";
body.push(line);
}
} else if (section === "BODY") {
if (line.startsWith('=')) {
checksum = line;
} else {
body.push(line);
}
}
}
// Reassemble: Header -> Attributes -> BLANK LINE -> Body -> Checksum -> Tail
let result = [headerLine];
if (headers.length > 0) result.push(...headers);
result.push(""); // The mandatory empty line
result.push(...body);
if (checksum) result.push(checksum);
result.push(tailLine);
return result.join('\n');
}
const readFileAsArrayBuffer = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
};
function loadOpenPGP(callback) {
if (window.openpgp) { callback(); return; }
jQuery.getScript(CDN_URL, function() { callback(); });
}
function isBinaryData(uint8) {
const header = Array.from(uint8.subarray(0, 4)).map(b => String.fromCharCode(b)).join('');
// Detect Zip (PK), PDF (%PDF), or PNG (0x89)
if (header.startsWith('PK') || header.startsWith('%PDF') || uint8[0] === 0x89) return true;
return uint8.subarray(0, 1024).some(byte => byte === 0);
}
const $wikiTextarea = jQuery('#wiki__text');
if ($wikiTextarea.length > 0) {
// Build UI
const $toolContainer = jQuery('
');
const $infoLabel = jQuery('🔐 PGP Engine:');
const $btnGroup = jQuery('');
const $encButton = jQuery('');
const $fileButton = jQuery('');
const $fileInput = jQuery('');
$btnGroup.append($encButton).append($fileButton).append($fileInput);
$toolContainer.append($infoLabel).append($btnGroup);
$wikiTextarea.before($toolContainer);
// Build Modal
const $modalOverlay = jQuery(`
`);
jQuery('body').append($modalOverlay);
let activeMacroResolve = null;
const handleConfirm = () => {
const val = jQuery('#pgpblock-modal-pass').val();
jQuery('#pgpblock-modal-pass').val('');
$modalOverlay.hide();
if (activeMacroResolve) activeMacroResolve(val);
};
jQuery('#pgpblock-modal-confirm').on('click', handleConfirm);
jQuery('#pgpblock-modal-cancel').on('click', () => { $modalOverlay.hide(); if (activeMacroResolve) activeMacroResolve(null); });
jQuery('#pgpblock-modal-pass').on('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleConfirm(); } });
function getPassphrase(title) {
return new Promise((res) => {
activeMacroResolve = res;
jQuery('#pgpblock-modal-title').text(title);
$modalOverlay.css('display', 'flex');
setTimeout(() => jQuery('#pgpblock-modal-pass').focus(), 50);
});
}
// Action: File Encrypt
$fileButton.on('click', () => $fileInput.click());
$fileInput.on('change', async function(e) {
const file = e.target.files[0];
if (!file) return;
const pass = await getPassphrase(`Encrypt File: ${file.name}`);
if (!pass) { $fileInput.val(''); return; }
$fileButton.prop('disabled', true).text('Encrypting...');
loadOpenPGP(async function() {
try {
const buf = await readFileAsArrayBuffer(file);
const msg = await window.openpgp.createMessage({ binary: new Uint8Array(buf), filename: file.name });
const enc = await window.openpgp.encrypt({ message: msg, passwords: [pass], format: 'armored' });
const tag = `\n${enc.trim()}\n\n`;
const el = $wikiTextarea[0], pos = el.selectionStart, text = $wikiTextarea.val();
$wikiTextarea.val(text.substring(0, pos) + tag + text.substring(pos));
} catch (err) { alert(err.message); }
finally { $fileButton.prop('disabled', false).text('📁 Encrypt & Insert File'); $fileInput.val(''); }
});
});
// Action: Encrypt/Decrypt
$encButton.on('click', async function(e) {
e.preventDefault();
const el = $wikiTextarea[0], caretPos = el.selectionStart, fullText = $wikiTextarea.val();
const pgpRegex = /<(pgp|gpg|pgpfile)([^>]*)>([\s\S]*?)<\/\1>/gi;
let blocks = []; let match;
while ((match = pgpRegex.exec(fullText)) !== null) {
const start = match.index, end = match.index + match[0].length;
let dist = (caretPos < start) ? (start - caretPos) : (caretPos > end ? caretPos - end : 0);
let fn = "";
const fnMatch = match[2].match(/filename=["']([^"']+)["']/i);
if (fnMatch) fn = fnMatch[1];
blocks.push({ start, end, tag: match[1], body: match[3].trim(), isEnc: match[3].includes('-----BEGIN PGP MESSAGE-----'), filename: fn, dist });
}
if (!blocks.length) return alert("No PGP blocks found.");
blocks.sort((a, b) => a.dist - b.dist);
const target = blocks[0];
let pass = await getPassphrase(target.isEnc ? "Decrypting Block" : "Encrypting Block");
if (!pass) return;
$encButton.prop('disabled', true).text('Processing...');
loadOpenPGP(async function() {
try {
let resultBody = "";
if (target.isEnc) {
// SANITIZE before readMessage
const armored = sanitizeArmor(target.body);
const msg = await window.openpgp.readMessage({ armoredMessage: armored });
const { data } = await window.openpgp.decrypt({
message: msg, passwords: [pass], format: 'binary',
config: { allowUnauthenticatedMessages: true }
});
if (target.tag === 'pgpfile' || isBinaryData(data)) {
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none'; a.href = url;
a.download = target.filename || "decrypted_file.bin";
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
resultBody = target.body;
} else {
resultBody = new TextDecoder().decode(data).trim();
}
} else {
const msg = await window.openpgp.createMessage({ text: target.body });
const enc = await window.openpgp.encrypt({ message: msg, passwords: [pass], format: 'armored' });
resultBody = enc.trim();
}
const attr = target.filename ? ` filename="${target.filename}"` : "";
const newBlock = `<${target.tag}${attr}>\n${resultBody}\n${target.tag}>`;
$wikiTextarea.val(fullText.substring(0, target.start) + newBlock + fullText.substring(target.end));
el.selectionStart = el.selectionEnd = target.start;
} catch (err) { alert("PGP Error: " + err.message); console.error(err); }
finally { $encButton.prop('disabled', false).text('🔐 Encrypt/Decrypt Closest'); }
});
});
}
});