release: v4.8.14 secure chat tools (code blocks, view-once, disappearing, unsend, panic)
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

New privacy-focused messaging controls in the composer:
- Code blocks: button wraps the message in a fenced block; both peers render a
  monospace code window with a copy button (clipboard auto-clears after ~30s).
  Window is built from sanitized text via React nodes — no new XSS surface.
- View-once: recipient sees a blurred bubble, reveals on tap, then it is wiped.
  Honestly cooperative (not screenshot-proof).
- Disappearing messages: optional 30s/5m/1h timer auto-deletes on both sides
  with a live countdown; incoming TTL clamped to [5s, 24h].
- Unsend (delete for everyone) via new MESSAGE_TYPES.message_delete control.
- Panic wipe: clears chat, wipes keys and disconnects (behind a confirm).

Transport:
- Per-message metadata (id / view-once / timer) travels inside the encrypted
  envelope, not in the sanitized text, so content cannot spoof these controls.
- _sanitizeMessageMeta whitelists + bounds metadata on send and receive.
- AAD/replay protection, SAS gate and receive-side DOMPurify are unchanged.

Adds tests/secure-chat-features.test.mjs (full suite: 17 files, all passing).
Bumps version to 4.8.14 across package.json, package-lock.json, manifest.json,
index.html, meta.json, README, SECURITY_DISCLAIMER, header and init banner.
This commit is contained in:
lockbitchat
2026-06-18 20:37:50 -04:00
parent cf36656341
commit 15173a9278
17 changed files with 1124 additions and 183 deletions
+60 -8
View File
@@ -6063,6 +6063,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
// Regular messages
MESSAGE: "message",
ENHANCED_MESSAGE: "enhanced_message",
// Per-message control (unsend / disappearing sync)
MESSAGE_DELETE: "message_delete",
// System messages
HEARTBEAT: "heartbeat",
VERIFICATION: "verification",
@@ -9986,7 +9988,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
}
return window.EnhancedSecureCryptoUtils.sanitizeMessage(message);
}
deliverMessageToUI(message, type = "received") {
deliverMessageToUI(message, type = "received", meta = null) {
try {
this._secureLog("debug", "\u{1F4E4} deliverMessageToUI called", {
message,
@@ -10052,8 +10054,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
}
const uiMessage = type === "received" ? this._sanitizeIncomingChatMessage(message) : message;
if (this.onMessage) {
const safeMeta = meta && typeof this._sanitizeMessageMeta === "function" ? this._sanitizeMessageMeta(meta) : null;
this._secureLog("debug", "\u{1F4E4} Calling this.onMessage callback", { message: uiMessage, type });
this.onMessage(uiMessage, type);
this.onMessage(uiMessage, type, safeMeta || void 0);
} else {
this._secureLog("warn", "\u26A0\uFE0F this.onMessage callback is null or undefined");
}
@@ -11069,7 +11072,42 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
return data;
}
}
async sendMessage(data) {
/**
* Whitelist + bound the per-message UI metadata so a peer cannot smuggle
* arbitrary objects, huge values, or absurd timers through it.
* @param {object} meta
* @returns {object|null}
*/
_sanitizeMessageMeta(meta) {
if (!meta || typeof meta !== "object") return null;
const out = {};
if (typeof meta.mid === "string" && meta.mid.length > 0 && meta.mid.length <= 64) {
out.mid = meta.mid.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64);
}
if (meta.code === true) out.code = true;
if (meta.once === true) out.once = true;
if (Number.isFinite(meta.ttl)) {
const ttl = Math.floor(meta.ttl);
if (ttl >= 5 && ttl <= 86400) out.ttl = ttl;
}
return Object.keys(out).length ? out : null;
}
/**
* Unsend: ask the peer to remove a previously delivered message by id.
* Sent over the authenticated DTLS control channel like other system
* messages. Best-effort and cooperative a peer can ignore it, exactly
* like WhatsApp/Telegram "delete for everyone".
* @param {string} messageId
* @returns {boolean}
*/
sendMessageDelete(messageId) {
if (typeof messageId !== "string" || !messageId) return false;
return this.sendSystemMessage({
type: _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_DELETE,
messageId: messageId.slice(0, 64)
});
}
async sendMessage(data, meta = null) {
const validation = this._validateInputData(data, "sendMessage");
if (!validation.isValid) {
const errorMessage = `Input validation failed: ${validation.errors.join(", ")}`;
@@ -11122,13 +11160,17 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
throw new Error("_createMessageAAD method is not available. Manager may not be fully initialized.");
}
const aad = this._createMessageAAD("message", { content: validation.sanitizedData });
return await this.sendSecureMessage({
const envelope = {
type: "message",
data: validation.sanitizedData,
timestamp: Date.now(),
aad
// Include AAD for sequence number validation
});
};
if (meta && typeof meta === "object") {
envelope.meta = this._sanitizeMessageMeta(meta);
}
return await this.sendSecureMessage(envelope);
}
this._secureLog("debug", "\u{1F510} Applying security layers to non-string data");
const securedData = await this._applySecurityLayersWithLimitedMutex(validation.sanitizedData, false);
@@ -11263,7 +11305,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
}
}
if (decryptedParsed.type === "message" && this.onMessage && decryptedParsed.data) {
this.deliverMessageToUI(decryptedParsed.data, "received");
this.deliverMessageToUI(decryptedParsed.data, "received", decryptedParsed.meta);
}
return;
} catch (error) {
@@ -11277,7 +11319,17 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
return;
}
if (this.onMessage && parsed.data) {
this.deliverMessageToUI(parsed.data, "received");
this.deliverMessageToUI(parsed.data, "received", parsed.meta);
}
return;
}
if (parsed.type === _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_DELETE) {
const messageId = parsed?.data?.messageId ?? parsed?.messageId;
if (typeof messageId === "string" && messageId) {
try {
this.onMessageDelete?.(messageId.slice(0, 64));
} catch (_) {
}
}
return;
}
@@ -17435,7 +17487,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("p", {
key: "subtitle",
className: "text-xs sm:text-sm text-muted hidden sm:block"
}, "End-to-end freedom v4.8.13")
}, "End-to-end freedom v4.8.14")
])
]),
// Status and Controls - Responsive