diff --git a/CHANGELOG.md b/CHANGELOG.md index fadef7d3..dbe63d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,17 @@ ADR-007 and ADR-002. ### Fixed +- **Secure Send now hands you a message, not a bare URL.** A link on its own + invites being pasted onto text already sitting in the chat box, and a chat + client turns the selection into the link's caption - so the secret travels as + the visible message and the link beside it protects nothing. That happened in + production: six LAPS passwords sent as the caption of a link whose whole job + was to keep them out of the chat. Copy message puts a sentence on the + clipboard instead, which has nowhere to attach itself, and the success card + now says plainly to delete the password from the chat box before pasting. + Nothing about the link itself was wrong - the URL carries only the token, the + key rides in the fragment, and neither ever reached the message. + - **Ticking a section off crossed out its title and nothing else.** The strike now covers everything under that heading, including its sub-sections, which is what "this section is done" means to somebody who ticked the thing that diff --git a/plugins/tools/frontend/views/SecureSendCreate.vue b/plugins/tools/frontend/views/SecureSendCreate.vue index a679db72..2074e417 100644 --- a/plugins/tools/frontend/views/SecureSendCreate.vue +++ b/plugins/tools/frontend/views/SecureSendCreate.vue @@ -94,8 +94,30 @@

+ + +
+ Send the link, not the secret. + If the password is already typed in a chat box, delete it before pasting + this. Pasting a link over selected text turns that text into the link's + caption, and the secret then travels in the message itself - the link + beside it protects nothing. +
+

This is the only time the link is shown - it is not stored anywhere you can look it up, and the key in it exists nowhere else. @@ -142,6 +164,7 @@ const busy = ref(false) const error = ref('') const link = ref('') const copied = ref(false) +const messagecopied = ref(false) const linkInput = ref(null) // Only nag once there is something to nag about: an empty box is not a mistake. @@ -222,6 +245,35 @@ async function copyLink() { } } +/** + * The link with a sentence around it. + * + * A bare URL invites being pasted onto text that is already in the chat box, + * and a chat client turns the selection into the link's caption - so the secret + * travels as the visible message and the link protects nothing. It has happened + * in production: six LAPS passwords sent as the caption of a link whose whole + * job was to keep them out of the chat. + * + * A sentence has nowhere to attach itself. Pasted into an empty box it is a + * message; pasted over a selection it replaces it rather than decorating it. + * Plain text on purpose - a rich-text flavour is what would let a secret ride + * along as markup. + */ +async function copyMessage() { + const opens = views.value === 1 + ? 'It opens once and then destroys itself' + : `It opens ${views.value} times and then destroys itself` + const message = + `Secure link - ${opens}, and expires ${expiryLabel.value}: ${link.value}` + try { + await navigator.clipboard.writeText(message) + messagecopied.value = true + setTimeout(() => { messagecopied.value = false }, 2000) + } catch (err) { + selectAll() + } +} + function selectAll() { linkInput.value?.select() } @@ -245,6 +297,17 @@ function reset() { .counter.over { color: var(--warning); font-weight: 600; } .control-note { color: var(--warning); font-size: 0.875rem; margin: 8px 0; } .control-hint { color: var(--text-light); font-size: 0.8125rem; margin: 8px 0 12px; } +/* Loud enough to be read before the paste, quiet enough not to look like an + error on a page where nothing went wrong. */ +.alert-send { + border-left: 3px solid var(--warning); + background: var(--bg-card-solid); + padding: 0.6rem 0.9rem; + margin: 0.75rem 0; + font-size: 0.875rem; + line-height: 1.5; +} +.alert-send strong { display: block; margin-bottom: 2px; } .link-row { display: flex; gap: 8px; align-items: center; margin: 0.5rem 0; } .link-row .form-control { flex: 1; } .mono { font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 0.875rem; }