add option to have unencrypted direct messages
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,24 +531,34 @@ 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;
|
||||
let sharedKey = null;
|
||||
if (_privateKey && currentOtherPubKey) {
|
||||
try {
|
||||
sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey);
|
||||
} catch (e) {
|
||||
return messages.map(m => ({ ...m, plaintext: null }));
|
||||
// Key derivation failed — will treat encrypted messages as null plaintext
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (const m of messages) {
|
||||
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 {
|
||||
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 { iv: newIv, ciphertext: newCt } = await encryptMessage(sharedKey, newText);
|
||||
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();
|
||||
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 = '<i class="fa-solid fa-paperclip"></i>';
|
||||
actions.prepend(attachBtn);
|
||||
@@ -1504,6 +1531,7 @@ if (window.__dmLoaded) {
|
||||
const file = attachInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!window.f0ckSession?.dm_unencrypted) {
|
||||
if (!currentOtherPubKey) {
|
||||
showFlashMsg('Cannot attach: recipient has no encryption key', 'error');
|
||||
return;
|
||||
@@ -1512,6 +1540,7 @@ if (window.__dmLoaded) {
|
||||
showFlashMsg('Cannot attach: your encryption key is not loaded', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show progress state on button
|
||||
const origHtml = attachBtn.innerHTML;
|
||||
@@ -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,6 +1862,7 @@ if (window.__dmLoaded) {
|
||||
? `${activeReply.quoteLines.join('\n')}\n\n${rawText}`
|
||||
: rawText;
|
||||
|
||||
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
|
||||
@@ -1839,12 +1872,21 @@ if (window.__dmLoaded) {
|
||||
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 {
|
||||
let ciphertext, iv;
|
||||
if (window.f0ckSession?.dm_unencrypted) {
|
||||
ciphertext = text;
|
||||
iv = 'unencrypted';
|
||||
} else {
|
||||
const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey);
|
||||
const { ciphertext, iv } = await encryptMessage(sharedKey, text);
|
||||
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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); },
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user