Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions assets/js/chat-key-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
let unlockedChatPrivateKey = null;
let unlockedChatSigningPrivateKey = null;
let pendingLoginPassword = null;
let conversationSubmitInFlight = false;
const state = {
status: "empty",
keyVersion: null,
Expand Down Expand Up @@ -1343,7 +1344,12 @@
);
return;
}
if (conversationSubmitInFlight) {
return;
}

conversationSubmitInFlight = true;
setConversationComposeEnabled(false);
setConversationStatus("Encrypting reply...");
try {
const encryptedCopies = {};
Expand Down Expand Up @@ -1384,6 +1390,9 @@
setConversationStatus("Reply sent.");
} catch (error) {
setConversationStatus("Reply could not be encrypted.");
} finally {
conversationSubmitInFlight = false;
setConversationComposeEnabled(root.dataset.canCompose === "true");
}
}

Expand Down
9 changes: 9 additions & 0 deletions hushline/static/js/chat-key-lifecycle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 21 additions & 8 deletions tests/playwright/e2ee/client-side-encryption.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,15 +455,27 @@ test("logged-in account conversation stays encrypted through browser lifecycle",
renderedPresenceCsrfToken,
);

const replyRequestPromise = recipientPage.waitForRequest(
(request) =>
request.method() === "POST" &&
request
.url()
.endsWith(`/conversation/${conversationPublicId}/messages`),
);
let replyRequestCount = 0;
const isReplyRequest = (request) =>
request.method() === "POST" &&
request.url().endsWith(`/conversation/${conversationPublicId}/messages`);
recipientPage.on("request", (request) => {
if (isReplyRequest(request)) {
replyRequestCount += 1;
}
});
const replyRequestPromise = recipientPage.waitForRequest(isReplyRequest);
await recipientPage.fill("#conversation-compose-body", replyPlaintext);
await recipientPage.locator("#conversation-compose-submit").click();
await recipientPage
.locator("#conversation-compose-form")
.evaluate((form) => {
form.dispatchEvent(
new Event("submit", { cancelable: true, bubbles: true }),
);
form.dispatchEvent(
new Event("submit", { cancelable: true, bubbles: true }),
);
});
const replyRequest = await replyRequestPromise;
const replyPostBody = replyRequest.postData() || "";
expectNoPlaintext(replyPostBody, sensitiveValues);
Expand Down Expand Up @@ -508,6 +520,7 @@ test("logged-in account conversation stays encrypted through browser lifecycle",
}

await expectConversationMessage(recipientPage, replyPlaintext);
expect(replyRequestCount).toBe(1);

const senderRefresh = senderPage.waitForResponse(
(response) =>
Expand Down
13 changes: 13 additions & 0 deletions tests/test_frontend_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,19 @@ def test_conversation_reply_submits_only_encrypted_copies() -> None:
assert expected_payload in static_js


def test_conversation_reply_prevents_overlapping_submissions() -> None:
js = (ROOT / "assets/js/chat-key-lifecycle.js").read_text(encoding="utf-8")
static_js = (ROOT / "hushline/static/js/chat-key-lifecycle.js").read_text(encoding="utf-8")

for bundle in (js, static_js):
assert "let conversationSubmitInFlight = false;" in bundle
assert "if (conversationSubmitInFlight)" in bundle
assert "conversationSubmitInFlight = true;" in bundle
assert "setConversationComposeEnabled(false);" in bundle
assert "finally" in bundle
assert bundle.count("conversationSubmitInFlight = false;") == 2


def test_conversation_polling_does_not_mark_messages_read() -> None:
js = (ROOT / "assets/js/chat-key-lifecycle.js").read_text(encoding="utf-8")
static_js = (ROOT / "hushline/static/js/chat-key-lifecycle.js").read_text(encoding="utf-8")
Expand Down
Loading