1jQuery(function() { 2 const CDN_URL = 'https://cdn.jsdelivr.net/npm/openpgp@5.11.0/dist/openpgp.min.js'; 3 4 /** 5 * Aggressive Sanitizer: This fixes the "Error during parsing" by 6 * reconstructing the PGP Armor block from scratch, ensuring the 7 * mandatory blank line after headers is present. 8 */ 9 function sanitizeArmor(text) { 10 const lines = text.trim().split(/\r?\n/); 11 let headerLine = ""; 12 let tailLine = ""; 13 let headers = []; 14 let body = []; 15 let checksum = ""; 16 let section = "START"; 17 18 for (let i = 0; i < lines.length; i++) { 19 let line = lines[i].trim(); 20 if (!line && section !== "BODY") continue; 21 22 if (line.startsWith('-----BEGIN PGP')) { 23 headerLine = line; 24 section = "HEADER"; 25 continue; 26 } 27 if (line.startsWith('-----END PGP')) { 28 tailLine = line; 29 section = "END"; 30 continue; 31 } 32 33 if (section === "HEADER") { 34 // If it looks like a header (contains ': '), keep it. 35 if (line.includes(': ')) { 36 headers.push(line); 37 } else if (/^[A-Za-z0-9+/]/.test(line)) { 38 // If it looks like Base64, the body started early (missing blank line) 39 section = "BODY"; 40 body.push(line); 41 } 42 } else if (section === "BODY") { 43 if (line.startsWith('=')) { 44 checksum = line; 45 } else { 46 body.push(line); 47 } 48 } 49 } 50 51 // Reassemble: Header -> Attributes -> BLANK LINE -> Body -> Checksum -> Tail 52 let result = [headerLine]; 53 if (headers.length > 0) result.push(...headers); 54 result.push(""); // The mandatory empty line 55 result.push(...body); 56 if (checksum) result.push(checksum); 57 result.push(tailLine); 58 59 return result.join('\n'); 60 } 61 62 const readFileAsArrayBuffer = (file) => { 63 return new Promise((resolve, reject) => { 64 const reader = new FileReader(); 65 reader.onload = () => resolve(reader.result); 66 reader.onerror = () => reject(reader.error); 67 reader.readAsArrayBuffer(file); 68 }); 69 }; 70 71 function loadOpenPGP(callback) { 72 if (window.openpgp) { callback(); return; } 73 jQuery.getScript(CDN_URL, function() { callback(); }); 74 } 75 76 function isBinaryData(uint8) { 77 const header = Array.from(uint8.subarray(0, 4)).map(b => String.fromCharCode(b)).join(''); 78 // Detect Zip (PK), PDF (%PDF), or PNG (0x89) 79 if (header.startsWith('PK') || header.startsWith('%PDF') || uint8[0] === 0x89) return true; 80 return uint8.subarray(0, 1024).some(byte => byte === 0); 81 } 82 83 const $wikiTextarea = jQuery('#wiki__text'); 84 85 if ($wikiTextarea.length > 0) { 86 // Build UI 87 const $toolContainer = jQuery('<div class="pgpblock-edit-tools"></div>'); 88 const $infoLabel = jQuery('<span class="pgp-info-label"> <b>PGP Engine:</b></span>'); 89 const $btnGroup = jQuery('<div class="pgp-modal-actions"></div>'); 90 const $encButton = jQuery('<button type="button" class="pgp-btn pgp-btn-text"> Encrypt/Decrypt Closest</button>'); 91 const $fileButton = jQuery('<button type="button" class="pgp-btn pgp-btn-file"> Encrypt & Insert File</button>'); 92 const $fileInput = jQuery('<input type="file" style="display:none;" />'); 93 94 $btnGroup.append($encButton).append($fileButton).append($fileInput); 95 $toolContainer.append($infoLabel).append($btnGroup); 96 $wikiTextarea.before($toolContainer); 97 98 // Build Modal 99 const $modalOverlay = jQuery(` 100 <div id="pgpblock-modal"> 101 <div class="pgp-modal-content"> 102 <h3 id="pgpblock-modal-title">PGP Cryptography</h3> 103 <p id="pgpblock-modal-desc">Enter passphrase:</p> 104 <input type="password" id="pgpblock-modal-pass" autocomplete="off" placeholder="Passphrase"/> 105 <div class="pgp-modal-actions"> 106 <button type="button" id="pgpblock-modal-cancel" class="pgp-btn pgp-btn-cancel">Cancel</button> 107 <button type="button" id="pgpblock-modal-confirm" class="pgp-btn pgp-btn-text">Submit</button> 108 </div> 109 </div> 110 </div> 111 `); 112 jQuery('body').append($modalOverlay); 113 114 let activeMacroResolve = null; 115 const handleConfirm = () => { 116 const val = jQuery('#pgpblock-modal-pass').val(); 117 jQuery('#pgpblock-modal-pass').val(''); 118 $modalOverlay.hide(); 119 if (activeMacroResolve) activeMacroResolve(val); 120 }; 121 jQuery('#pgpblock-modal-confirm').on('click', handleConfirm); 122 jQuery('#pgpblock-modal-cancel').on('click', () => { $modalOverlay.hide(); if (activeMacroResolve) activeMacroResolve(null); }); 123 jQuery('#pgpblock-modal-pass').on('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleConfirm(); } }); 124 125 function getPassphrase(title) { 126 return new Promise((res) => { 127 activeMacroResolve = res; 128 jQuery('#pgpblock-modal-title').text(title); 129 $modalOverlay.css('display', 'flex'); 130 setTimeout(() => jQuery('#pgpblock-modal-pass').focus(), 50); 131 }); 132 } 133 134 // Action: File Encrypt 135 $fileButton.on('click', () => $fileInput.click()); 136 $fileInput.on('change', async function(e) { 137 const file = e.target.files[0]; 138 if (!file) return; 139 const pass = await getPassphrase(`Encrypt File: ${file.name}`); 140 if (!pass) { $fileInput.val(''); return; } 141 $fileButton.prop('disabled', true).text('Encrypting...'); 142 loadOpenPGP(async function() { 143 try { 144 const buf = await readFileAsArrayBuffer(file); 145 const msg = await window.openpgp.createMessage({ binary: new Uint8Array(buf), filename: file.name }); 146 const enc = await window.openpgp.encrypt({ message: msg, passwords: [pass], format: 'armored' }); 147 const tag = `<pgpfile filename="${file.name}">\n${enc.trim()}\n</pgpfile>\n`; 148 const el = $wikiTextarea[0], pos = el.selectionStart, text = $wikiTextarea.val(); 149 $wikiTextarea.val(text.substring(0, pos) + tag + text.substring(pos)); 150 } catch (err) { alert(err.message); } 151 finally { $fileButton.prop('disabled', false).text(' Encrypt & Insert File'); $fileInput.val(''); } 152 }); 153 }); 154 155 // Action: Encrypt/Decrypt 156 $encButton.on('click', async function(e) { 157 e.preventDefault(); 158 const el = $wikiTextarea[0], caretPos = el.selectionStart, fullText = $wikiTextarea.val(); 159 const pgpRegex = /<(pgp|gpg|pgpfile)([^>]*)>([\s\S]*?)<\/\1>/gi; 160 let blocks = []; let match; 161 162 while ((match = pgpRegex.exec(fullText)) !== null) { 163 const start = match.index, end = match.index + match[0].length; 164 let dist = (caretPos < start) ? (start - caretPos) : (caretPos > end ? caretPos - end : 0); 165 let fn = ""; 166 const fnMatch = match[2].match(/filename=["']([^"']+)["']/i); 167 if (fnMatch) fn = fnMatch[1]; 168 blocks.push({ start, end, tag: match[1], body: match[3].trim(), isEnc: match[3].includes('-----BEGIN PGP MESSAGE-----'), filename: fn, dist }); 169 } 170 171 if (!blocks.length) return alert("No PGP blocks found."); 172 blocks.sort((a, b) => a.dist - b.dist); 173 const target = blocks[0]; 174 175 let pass = await getPassphrase(target.isEnc ? "Decrypting Block" : "Encrypting Block"); 176 if (!pass) return; 177 178 $encButton.prop('disabled', true).text('Processing...'); 179 loadOpenPGP(async function() { 180 try { 181 let resultBody = ""; 182 if (target.isEnc) { 183 // SANITIZE before readMessage 184 const armored = sanitizeArmor(target.body); 185 const msg = await window.openpgp.readMessage({ armoredMessage: armored }); 186 const { data } = await window.openpgp.decrypt({ 187 message: msg, passwords: [pass], format: 'binary', 188 config: { allowUnauthenticatedMessages: true } 189 }); 190 191 if (target.tag === 'pgpfile' || isBinaryData(data)) { 192 const blob = new Blob([data], { type: 'application/octet-stream' }); 193 const url = URL.createObjectURL(blob); 194 const a = document.createElement('a'); 195 document.body.appendChild(a); 196 a.style.display = 'none'; a.href = url; 197 a.download = target.filename || "decrypted_file.bin"; 198 a.click(); 199 document.body.removeChild(a); 200 URL.revokeObjectURL(url); 201 resultBody = target.body; 202 } else { 203 resultBody = new TextDecoder().decode(data).trim(); 204 } 205 } else { 206 const msg = await window.openpgp.createMessage({ text: target.body }); 207 const enc = await window.openpgp.encrypt({ message: msg, passwords: [pass], format: 'armored' }); 208 resultBody = enc.trim(); 209 } 210 211 const attr = target.filename ? ` filename="${target.filename}"` : ""; 212 const newBlock = `<${target.tag}${attr}>\n${resultBody}\n</${target.tag}>`; 213 $wikiTextarea.val(fullText.substring(0, target.start) + newBlock + fullText.substring(target.end)); 214 el.selectionStart = el.selectionEnd = target.start; 215 } catch (err) { alert("PGP Error: " + err.message); console.error(err); } 216 finally { $encButton.prop('disabled', false).text(' Encrypt/Decrypt Closest'); } 217 }); 218 }); 219 } 220}); 221