Skip to content

Add end-to-end encrypted private media - #82

Open
Danidfra wants to merge 12 commits into
lnbits:mainfrom
Danidfra:feat/encrypted-private-media-brutal
Open

Danidfra wants to merge 12 commits into
lnbits:mainfrom
Danidfra:feat/encrypted-private-media-brutal

Conversation

@Danidfra

@Danidfra Danidfra commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

  • Private media (DMs and private groups) is end-to-end encrypted by default: AES-256-GCM on the device, ciphertext uploaded to Blossom, sent as NIP-17 kind 15. Public groups are unchanged.
  • Upload failures name the cause and offer Retry, Change server, or Cancel. Nothing is retried or downgraded automatically.
  • Security: "Send without encryption" is available only after explicit confirmation. The file is then visible to the Blossom operator and to anyone with the URL.

Testing

Unit and E2E tests added (encryption round-trip, consent flow); check and build pass.

Rebased onto upstream/main a4db9c6; the only conflicts were the imports in messageAttachments.ts (upstream's roomLinkStatus plus the encryption imports) and the e2e parity helper (upstream's version).

Compatibility: clients that only support kind 14 attachments won't show this media.

@dadofsambonzuki

Copy link
Copy Markdown

Heads-up on overlap, since this branch rewrites createBlossomSettingsRuntime.

#92 reads the account's published Blossom server list (kind 10063, BUD-03) for uploads: saved setting → first server of the published list → built-in default. It touches the same file, but only adds one dependency (readPublishedBlossomServerUrls) and the three-line resolution inside getBlossomServerUrl, plus a new blossomServerListRuntime.ts next to myRelayListRuntime.

It does not touch the encrypted private-media preference, so the two changes are additive rather than competing. Whichever lands second should be a small rebase; happy to rebase #92 onto this one if you would rather merge this first.

@Danidfra

Copy link
Copy Markdown
Author

Thanks for checking!

I think it makes sense to merge #82 first and rebase #92 afterward, if that works for you

@Danidfra
Danidfra force-pushed the feat/encrypted-private-media-brutal branch from 53d081e to d3bd16a Compare October 15, 2026 03:12
@Danidfra
Danidfra changed the base branch from brutal to main October 15, 2026 03:12
@Danidfra
Danidfra changed the base branch from main to brutal October 15, 2026 03:18
@Danidfra
Danidfra changed the base branch from brutal to main October 15, 2026 03:18
@Danidfra
Danidfra force-pushed the feat/encrypted-private-media-brutal branch from d3bd16a to ba71ac4 Compare October 21, 2026 02:39
Private media is encrypted on the device with AES-256-GCM, uploaded as
ciphertext to the encrypted-media Blossom server and sent as a NIP-17
kind 15 file message. Receivers verify the ciphertext hash before
decrypting and only render allowlisted media types.

Rebased onto main as one commit (previously cfdeaeb, b25b272, d3bd16a);
the removed Notify calls now use reportActionFeedback.
Testing a server uploaded random bytes, downloaded them and asked to
delete them, which needed a separate delete authorization. The real
upload already reports a server that rejects or alters encrypted files,
so the probe, its delete auth action and its strings are removed.
Uploads now fail with a BlossomUploadError that names the cause: network,
authentication (including a signer that cannot sign), payment required,
file too large, unsupported content type, server error, invalid response
or integrity failure. A 415 for encrypted media says the server does not
accept encrypted files instead of blaming every failure on encryption.

Server reasons are shown only as short plain text; HTML error pages and
long bodies are dropped. Plaintext and public uploads share the same PUT
and classification.
Deletes the upload session, plaintext notice preference and
send-then-persist modules with their unit tests, and drops the
paste/drop source argument from the composer's file callback. Their
remaining callers move to the explicit unencrypted upload fallback in
the next commit.
Private media stays encrypted by default. When an encrypted upload fails,
the card offers Retry, Change server, Send without encryption and Cancel;
a file that cannot be encrypted offers Send without encryption and
Cancel. Plaintext is only uploaded after that choice, and the first time
after a confirmation explaining that the message stays NIP-17 encrypted
while the file itself is not. The file goes to the regular Blossom server
and is sent as the existing kind 14 + imeta attachment.

"Don't ask again" is saved in the account's private preferences, skips
only that confirmation, and can be reset in Media settings. It never
selects plaintext by itself.

The upload session and send-then-save helper are folded into the
composer upload controller, and the encryption notice preference is
removed: the notice is part of the upload confirmation.
QuickTime video, the iPhone camera format, is now encrypted like MP4 and
WebM: Chromium and WebKit play H.264 MOV from the decrypted blob, and it
goes through the same media decoder. Audio uses the same 20 MiB limit as
images, video and plaintext uploads, so the per-type limits collapse into
one. Common aliases browsers report (audio/x-m4a, audio/mp3, audio/x-wav)
resolve to their canonical types.

HEIC stays unsupported because most browsers cannot display it; it and
anything over 20 MiB can still be sent without encryption after the user
chooses to.
Check UI state instead of fixed timeouts, use a committed WebM fixture instead of recording one, and drop the pasted kind 15 test that repeated the composer upload test.
@Danidfra
Danidfra force-pushed the feat/encrypted-private-media-brutal branch from 9e5059d to 97575d2 Compare October 23, 2026 16:55

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants