diff --git a/config_example.json b/config_example.json index aee960a..86a1325 100644 --- a/config_example.json +++ b/config_example.json @@ -62,6 +62,7 @@ "enable_danmaku": true, "private_messages": true, "dm_attachments": true, + "dm_unencrypted": false, "dm_attachment_expiry_days": 90, "halls_enabled": true, "userhalls_enabled": true, diff --git a/public/s/js/messages.js b/public/s/js/messages.js index ff80481..6be376a 100644 --- a/public/s/js/messages.js +++ b/public/s/js/messages.js @@ -344,7 +344,7 @@ if (window.__dmLoaded) { // Try to get recipient's key — but don't block if they don't have one yet currentOtherPubKey = await getRemotePublicKey(currentOtherId); - if (!currentOtherPubKey) { + if (!window.f0ckSession?.dm_unencrypted && !currentOtherPubKey) { // Recipient has no key — clearly block sending const notice = document.getElementById('dm-key-notice'); if (notice) { @@ -531,23 +531,33 @@ if (window.__dmLoaded) { async function decryptBatch(messages) { if (!messages.length) return []; - if (!currentOtherPubKey) return messages.map(m => ({ ...m, plaintext: null })); - // Derive shared key ONCE for the whole batch — ECDH is expensive, don't repeat per message - let sharedKey; - try { - sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); - } catch (e) { - return messages.map(m => ({ ...m, plaintext: null })); + let sharedKey = null; + if (_privateKey && currentOtherPubKey) { + try { + sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); + } catch (e) { + // Key derivation failed — will treat encrypted messages as null plaintext + } } const result = []; for (const m of messages) { - try { - const plaintext = await decryptMessage(sharedKey, m.ciphertext, m.iv); - result.push({ ...m, plaintext }); - } catch (e) { - result.push({ ...m, plaintext: null }); + if (m.iv === 'unencrypted' || !m.iv) { + // Unencrypted message + result.push({ ...m, plaintext: m.ciphertext }); + } else { + // Encrypted message + if (sharedKey) { + try { + const plaintext = await decryptMessage(sharedKey, m.ciphertext, m.iv); + result.push({ ...m, plaintext }); + } catch (e) { + result.push({ ...m, plaintext: null }); + } + } else { + result.push({ ...m, plaintext: null }); + } } } return result; @@ -906,13 +916,21 @@ if (window.__dmLoaded) { div.dataset.editing = ''; return; } - if (!currentOtherPubKey) { showFlashMsg('Cannot encrypt: no key', 'error'); return; } saveBtn.disabled = true; saveBtn.textContent = '…'; try { - const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); - const { iv: newIv, ciphertext: newCt } = await encryptMessage(sharedKey, newText); + let newIv, newCt; + if (window.f0ckSession?.dm_unencrypted) { + newIv = 'unencrypted'; + newCt = newText; + } else { + if (!currentOtherPubKey) { showFlashMsg('Cannot encrypt: no key', 'error'); saveBtn.disabled = false; saveBtn.textContent = 'Save'; return; } + const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); + const enc = await encryptMessage(sharedKey, newText); + newIv = enc.iv; + newCt = enc.ciphertext; + } const res = await fetch(`/api/dm/message/${m.id}`, { method: 'PATCH', @@ -1107,7 +1125,12 @@ if (window.__dmLoaded) { let iv, encryptedBuffer; try { const raw = await file.arrayBuffer(); - ({ iv, encryptedBuffer } = await encryptAttachment(sharedKey, raw)); + if (window.f0ckSession?.dm_unencrypted) { + iv = 'unencrypted'; + encryptedBuffer = raw; + } else { + ({ iv, encryptedBuffer } = await encryptAttachment(sharedKey, raw)); + } } catch (e) { showFlashMsg('Encryption failed: ' + e.message, 'error'); return null; @@ -1405,12 +1428,7 @@ if (window.__dmLoaded) { // Shared fetch+decrypt helper — returns a Blob or null async function fetchAndDecryptAttachment(id, mime) { - if (!currentOtherPubKey) { - showFlashMsg('Cannot decrypt: no encryption key', 'error'); - return null; - } try { - const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); const res = await fetch(`/api/dm/attachment/${id}`, { headers: { 'X-CSRF-Token': csrfToken() } }); @@ -1420,6 +1438,15 @@ if (window.__dmLoaded) { if (!ivHeader) { showFlashMsg('Server did not return IV — please refresh', 'error'); return null; } const encBuf = await res.arrayBuffer(); + if (ivHeader === 'unencrypted') { + return new Blob([encBuf], { type: mime || 'application/octet-stream' }); + } + + if (!currentOtherPubKey) { + showFlashMsg('Cannot decrypt: no encryption key', 'error'); + return null; + } + const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); const plainBuf = await decryptAttachment(sharedKey, ivHeader, new Uint8Array(encBuf)); return new Blob([plainBuf], { type: mime || 'application/octet-stream' }); } catch (e) { @@ -1489,7 +1516,7 @@ if (window.__dmLoaded) { const attachBtn = document.createElement('button'); attachBtn.type = 'button'; - attachBtn.title = 'Send encrypted attachment'; + attachBtn.title = window.f0ckSession?.dm_unencrypted ? 'Send attachment' : 'Send encrypted attachment'; attachBtn.className = 'dm-attach-btn'; attachBtn.innerHTML = ''; actions.prepend(attachBtn); @@ -1504,13 +1531,15 @@ if (window.__dmLoaded) { const file = attachInput.files[0]; if (!file) return; - if (!currentOtherPubKey) { - showFlashMsg('Cannot attach: recipient has no encryption key', 'error'); - return; - } - if (!hasKey()) { - showFlashMsg('Cannot attach: your encryption key is not loaded', 'error'); - return; + if (!window.f0ckSession?.dm_unencrypted) { + if (!currentOtherPubKey) { + showFlashMsg('Cannot attach: recipient has no encryption key', 'error'); + return; + } + if (!hasKey()) { + showFlashMsg('Cannot attach: your encryption key is not loaded', 'error'); + return; + } } // Show progress state on button @@ -1524,7 +1553,10 @@ if (window.__dmLoaded) { }; try { - const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); + let sharedKey = null; + if (!window.f0ckSession?.dm_unencrypted) { + sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); + } const sentinel = await uploadDmAttachment(file, sharedKey, currentOtherId, onProgress); if (sentinel) { // Append sentinel to the textarea (with a newline separator) @@ -1540,7 +1572,7 @@ if (window.__dmLoaded) { } finally { attachBtn.disabled = false; attachBtn.innerHTML = origHtml; - attachBtn.title = 'Send encrypted attachment'; + attachBtn.title = window.f0ckSession?.dm_unencrypted ? 'Send attachment' : 'Send encrypted attachment'; attachInput.value = ''; } }); @@ -1830,21 +1862,31 @@ if (window.__dmLoaded) { ? `${activeReply.quoteLines.join('\n')}\n\n${rawText}` : rawText; - // If recipient has no key, block sending — we cannot encrypt for them - if (!currentOtherPubKey) { - // Re-check in case they registered since page load - currentOtherPubKey = await getRemotePublicKey(currentOtherId); - } - if (!currentOtherPubKey) { - showFlashMsg('This user hasn\'t set up their encryption key yet. Sending is not possible until they do.', 'error'); - return; + if (!window.f0ckSession?.dm_unencrypted) { + // If recipient has no key, block sending — we cannot encrypt for them + if (!currentOtherPubKey) { + // Re-check in case they registered since page load + currentOtherPubKey = await getRemotePublicKey(currentOtherId); + } + if (!currentOtherPubKey) { + showFlashMsg('This user hasn\'t set up their encryption key yet. Sending is not possible until they do.', 'error'); + return; + } } sendInFlight = true; btn.disabled = true; try { - const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); - const { ciphertext, iv } = await encryptMessage(sharedKey, text); + let ciphertext, iv; + if (window.f0ckSession?.dm_unencrypted) { + ciphertext = text; + iv = 'unencrypted'; + } else { + const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey); + const enc = await encryptMessage(sharedKey, text); + ciphertext = enc.ciphertext; + iv = enc.iv; + } const res = await dmFetch('POST', `/api/dm/send/${currentOtherId}`, { ciphertext, iv }); if (res.success) { input.value = ''; @@ -1880,6 +1922,11 @@ if (window.__dmLoaded) { // ───────────────────────────────────────────────────────────────────────── async function ensureKeyReady() { + if (window.f0ckSession?.dm_unencrypted) { + const btns = document.querySelectorAll('.dm-manage-keys-btn'); + btns.forEach(btn => btn.style.display = 'none'); + return; + } const status = await loadOrCreateKeyPair(); // Always re-upload public key silently (on every page, so recipients can find us) uploadPublicKey().catch(e => console.warn('[DM] pubkey upload failed:', e)); diff --git a/src/inc/settings.mjs b/src/inc/settings.mjs index 93c219c..85fa7d6 100644 --- a/src/inc/settings.mjs +++ b/src/inc/settings.mjs @@ -8,6 +8,7 @@ let bypass_duplicate_check = false; let protect_files = false; let private_messages = true; let dm_attachments = true; +let dm_unencrypted = false; let default_layout = 'modern'; let enable_pdf = false; let enable_cleanup = false; @@ -63,6 +64,9 @@ export const setPrivateMessages = (val) => private_messages = !!val; export const getDmAttachments = () => dm_attachments; export const setDmAttachments = (val) => dm_attachments = !!val; +export const getDmUnencrypted = () => dm_unencrypted; +export const setDmUnencrypted = (val) => dm_unencrypted = !!val; + export const getDmAttachmentExpiryDays = () => { const v = parseInt(cfg.websrv.dm_attachment_expiry_days); return (Number.isFinite(v) && v > 0) ? v : 90; diff --git a/src/index.mjs b/src/index.mjs index bc6a5a6..207497f 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -18,7 +18,7 @@ import { handleMetaExtract } from "./meta_extract_handler.mjs"; import { handleMetaStrip } from "./meta_strip_handler.mjs"; import { handleCommentUpload } from "./comment_upload_handler.mjs"; import { handleDmAttachmentUpload, handleDmAttachmentDownload, handleDmAttachmentDelete } from "./dm_attachment_handler.mjs"; -import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDmAttachments, setDmAttachments, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs"; +import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDmAttachments, setDmAttachments, getDmUnencrypted, setDmUnencrypted, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs"; import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs"; import { createI18n } from "./inc/i18n.mjs"; import security from "./inc/security.mjs"; @@ -1007,6 +1007,10 @@ process.on('uncaughtException', err => { // Default is true; requires private_messages to also be enabled setDmAttachments(cfg.websrv.dm_attachments !== false); console.log(`[BOOT] DM attachments: ${cfg.websrv.dm_attachments !== false ? 'ENABLED' : 'DISABLED'}`); + + // Load dm_unencrypted from config.json (static — not a DB setting) + setDmUnencrypted(!!cfg.websrv.dm_unencrypted); + console.log(`[BOOT] DM unencrypted: ${cfg.websrv.dm_unencrypted ? 'ENABLED' : 'DISABLED'}`); // Load default_layout from config.json (static) if (cfg.websrv.default_layout) { setDefaultLayout(cfg.websrv.default_layout); @@ -1094,6 +1098,7 @@ process.on('uncaughtException', err => { enable_profile_description: !!cfg.websrv.enable_profile_description, get private_messages() { return getPrivateMessages(); }, get dm_attachments() { return getDmAttachments(); }, + get dm_unencrypted() { return getDmUnencrypted(); }, get enable_pdf() { return getEnablePdf(); }, get enable_cleanup() { return getEnableCleanup(); }, get cleanup_start_date() { return getCleanupStartDate(); }, diff --git a/views/snippets/footer.html b/views/snippets/footer.html index d6fc53c..04f41d2 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -412,7 +412,8 @@ fileupload_comments_size: {{ fileupload_comments_size }}, fileupload_comments_max: {{ fileupload_comments_max }}, fileupload_comments_mode: "{{ fileupload_comments_mode }}", - dm_attachments: @if(dm_attachments) true @else false @endif + dm_attachments: @if(dm_attachments) true @else false @endif, + dm_unencrypted: @if(dm_unencrypted) true @else false @endif }; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckI18n = {