Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Chat attachments: send, receive and preview images and PDFs, interoperable with v1 (Blossom)

Open
#589 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
30/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
dart, flutter, rust

Research direction

Start with crypto/file_enc.rs, api/messages.rs, and nostr/blossom.rs to understand the existing attachment path; the issue divides the work into separate Rust, Dart UI, dispute-chat, web, and image-UX phases. For phase 1, add the v1 cross-client fixtures and run cargo test and cargo clippy. Done means v1 and v2 exchange attachments successfully, downloaded content is verified and cached encrypted, and uploads are not signed by the identity key.

Written by the indexing model from the issue text.

Description

area: chat area: ui feature

Problem

v2 cannot send, receive or preview images or PDFs in the trade chat. In the UI:

  • The paperclip is a stub (chat_room_screen.dart _onAttach: "UI hook deferred").
  • encrypted_image_message.dart and encrypted_file_message.dart are placeholders.
  • An attachment sent from v1 shows up as JSON text (see below).

In a P2P trade the chat is where the payment proof travels (a transfer screenshot, a bank PDF), so today a v2 user cannot send the evidence the counterpart — or a solver — asks for.

v1 (MostroP2P/mobile v1.4.2) supports this with client-side encryption and Blossom storage, and the idea is sound. This issue describes how v1 does it, what v2 already has, why that code does not work with v1 yet, and the step-by-step plan, before implementing.

Related: #140 (UI wiring), #130 (image UX), #150 (web Blossom), #143 (dispute chat). #140's premise — "the Rust side is in place" — does not hold: see What v2 already has. This issue is meant as the umbrella for all four.

How v1 does it

Checked in MostroP2P/mobile @ v1.4.2 (637fd43):

  1. Pick. ChatFileUploadHelper (lib/shared/utils/chat_file_upload_helper.dart) opens FilePicker limited to JPG/PNG, PDF/DOC/DOCX and MP4/MOV/AVI, checks the 25 MB limit before reading the file, and asks for confirmation.
  2. Sanitize images. MediaValidationService.validateAndSanitizeImageLight decodes and re-encodes JPEG/PNG in an isolate, which drops EXIF/GPS metadata, and returns width/height. Documents get structural checks (the %PDF- header; DOC/DOCX without macros).
  3. Encrypt. ChaCha20-Poly1305, output [nonce 12][ciphertext][tag 16]. Key: the raw ECDH secret of the two trade keys (Nip44.computeSharedSecret → session.sharedKey → sharedKeyToBytes), the same secret the chat envelope derives K_conv/K_sign from. It is not K_conv, and not SHA-256 of the secret. In the dispute chat the key is the raw ECDH secret between the trade key and the solver (adminSharedKey).
  4. Upload to Blossom.
    • PUT {server}/upload (BUD-02), Content-Type: application/octet-stream.
    • Auth is a kind 24242 event signed by a fresh random key per upload: not the identity key, not the trade key.
    • The URL is built as {server}/{sha256}.
    • Servers are tried in order (BlossomConfig.defaultServers): cdn.hzrd149.com, nostr.download, blossom-01.uid.ovh, files.sovbit.host, blssm.us. The config's own comment explains the choice: they accept opaque blobs (media-only servers reject encrypted data by sniffing it), and they keep files indefinitely, because dispute evidence must not expire.
  5. Send. The chat message is a normal chat-envelope message (kind 14 / inner kind 1) whose text is a JSON string:
{"type":"image_encrypted","blossom_url":"https://…/<sha256>","nonce":"<hex>","mime_type":"image/jpeg",
 "original_size":524288,"width":1920,"height":1080,"filename":"…","encrypted_size":524320}
{"type":"file_encrypted","file_type":"document","blossom_url":"https://…/<sha256>","nonce":"<hex>",
 "mime_type":"application/pdf","original_size":1048576,"filename":"…","encrypted_size":1048608}

The nonce field repeats the one at the head of the blob. v1's parser casts every field with as int / as String, so an image_encrypted without width/height breaks on the v1 side.
6. Receive. A message whose text parses as JSON with one of those types is rendered as an attachment.

  • Images are downloaded and decrypted automatically for preview.
  • Other files show a card and download only on demand, then "Open" writes a temp file and calls OpenFile.
  • The cache is memory-only; v1's own doc (.specify/v1-reference/ENCRYPTED_IMAGE_MESSAGING_IMPLEMENTATION.md, Future Improvements) says a disk cache must keep the encrypted blob and decrypt only at display time.

What v2 already has, and why it does not work with v1

Rust has nostr/blossom.rs, crypto/file_enc.rs (same blob layout as v1), api/messages.rs::send_file / download_attachment / parse_chat_payload, and the on_attachment_progress stream. None of it is reachable from the UI, and as written it would not work with v1:

v1 v2 today Effect
File key raw ECDH x-coordinate derive_nip04_shared_key = SHA-256 of it (orders.rs peer-reveal, send_file) Neither side can decrypt the other's files
Message JSON image_encrypted / file_encrypted, fields above {"type":"file","url","name","mime_type","size"} v1 files arrive in v2 as raw JSON text; v2 files never render in v1
Upload endpoint PUT /upload PUT /{sha256} Works only on servers that still take the old form
Upload auth fresh key per upload the identity key (get_active_keys) Privacy: every upload is signed by the user's long-lived identity, linking their trades on every Blossom server
Servers the 5 above (opaque blobs, long retention) older list incl. blossom.primal.net, nostr.media… v1 dropped those: media-only (they reject encrypted blobs) or ephemeral (evidence expires)
Downloaded file memory decrypted file written to a temp path Plaintext payment proof left on disk
Integrity — — Neither checks that the downloaded blob's SHA-256 matches the URL
Web — NotImplemented (#150) No attachments on the web build

(Checked by reading both codebases; the ECDH form is also stated in crypto/chat_keys.rs. Step 1 below pins it with a cross-client test vector before anything ships.)

Open decision — can a solver open P2P chat files?

v1 discloses K_conv to a solver during a dispute, as the chat spec says (read-only access), and deliberately never the raw ECDH secret (it also derives K_sign). But v1 encrypts P2P chat files with the raw secret. So a solver can read the whole conversation, see that a receipt was sent, and cannot open it. The payment proof, the main reason for attachments, is invisible to whoever decides the dispute. Files sent in the dispute chat itself are fine: the solver holds its side of that ECDH.

Options:

  • A — v1-compatible (raw ECDH). Works with v1 today; keeps the gap.
  • B — a file key derived from K_conv (e.g. HKDF-SHA256(K_conv, info = "mostro:chat:file:v1")). The solver can open the files; needs a protocol addition and the same change in v1, or v1 and v2 stop reading each other's files.

Recommendation: ship A for interoperability, and open a protocol proposal for B, to be adopted by both clients together (with a version bump of the type field so old and new files coexist).

Step-by-step plan

Each step is its own PR, feat(chat-attachments): phase N.

Phase 1 — Rust core, wire-compatible with v1 (no UI)
  1. Key: the file key is the raw ECDH secret for the peer chat and the raw ECDH with the solver for the dispute chat. Add a cross-client test vector: a blob encrypted by v1's EncryptionService, decrypted by file_enc with keys derived from fixed trade keys, and the reverse direction.
  2. Wire format: a chat_attachment module with serde types for image_encrypted and file_encrypted, byte-for-byte with v1 (field names, required width/height, the redundant nonce, file_type ∈ {image, video, document}). parse_chat_payload recognizes both; send_file emits them. The v2 type:"file" shape was never sent (the UI was never wired), so it is dropped, not migrated. Tests use v1 JSON fixtures.
  3. Blossom client:
    • PUT /upload (BUD-02) with application/octet-stream, auth signed by a fresh key per upload.
    • v1's server list.
    • Take the URL from the server's blob descriptor, falling back to {server}/{sha256}.
    • On download: verify SHA-256 against the URL before decrypting, reject over 25 MB by Content-Length and by bytes, accept https:// (and blossom:// if seen).
  4. Validation and sanitization in Rust (the core decides what leaves the device):
    • Detect the type by magic bytes, never by extension or peer-supplied MIME.
    • Allowed to send: JPEG, PNG, PDF.
    • Images are decoded and re-encoded (drops EXIF/GPS) with a pixel cap against decompression bombs, returning width/height.
    • PDFs must start with %PDF-.
    • Received files of every v1 type are accepted, including DOC/DOCX and video.
  5. Encrypted at rest: the cache stores the encrypted blob keyed by SHA-256. download_attachment returns the decrypted bytes (FRB Vec<u8>) instead of a plaintext temp path. The cache is identity-scoped: it is added to clear_identity_data / resetIdentityScopedState (CLAUDE.md, A new identity starts from zero).
  6. Real progress: feed on_attachment_progress from bytes sent and received, and report an indeterminate state without Content-Length.
  7. Spec: FR-036 in detail (types, limits, auto-download rules, encrypted cache), data-model.md and contracts/messages.md with the two JSON shapes. The design doc links to v1's.
Phase 2 — Dart UI (the #140 scope)
  1. Paperclip. A sheet with Photo / Camera / File (PDF), the size check before reading, a confirmation dialog, and an upload bubble with progress plus cancel/retry.
  2. Image bubble. Auto-download and decrypt on arrival (as v1 does), a thumbnail sized by width/height, loading and error states with retry.
  3. Full-screen viewer. Pinch-zoom, save to gallery, share.
  4. PDF bubble. Name, size and icon; tap to download and decrypt, then an in-app preview rendered from memory (first page as a thumbnail, full viewer), plus "open with…" and save.
  5. Other v1 types (DOC/DOCX/video). Received ones show a file card with open/save; v2 does not send them in this issue.
  6. Untrusted names. The peer-supplied filename is sanitized (no path separators, no traversal) before any save or temp file. Temp files exist only for "open with…" and are deleted afterwards.
  7. Rest of the UI. l10n in all 6 languages; semantics labels; golden images in the PR (light and dark, image and PDF bubbles, the upload state).
  8. Tests. Widget tests for each state.
Phase 3 — Dispute chat (with #143)

The same attach path in the dispute chat, keyed to the solver. Blocked on #143's dispute wiring.

Phase 4 — Web (#150)

A fetch-based Blossom client for wasm (the auth construction does not change). Check the servers' CORS, cache the encrypted blob in IndexedDB, and extend the web smoke test with an encrypted upload and read-back.

Phase 5 — Image UX (#130)

Compression and max-edge resize before upload, and a thumbnail before full decrypt.

Acceptance

  • An image and a PDF sent from v2 open in v1 v1.4.2, and v1's open in v2, in both directions (manual cross-client test; notes in the PR).
  • Nothing on disk holds a decrypted attachment, except a temp file created by an explicit "open with…" and deleted afterwards.
  • Blossom uploads are not signed by the identity key.
  • cargo test, cargo clippy, flutter analyze and flutter test pass; goldens are in the PR.

Out of scope

Video playback in-app, sending DOC/DOCX/video, and the protocol change of option B (tracked separately once decided).

Dominant language
Dart
Stars
11
Forks
9
Avg merge
13h 4m
Merged PRs (30d)
259

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from MostroP2P/app

All issues in MostroP2P/app

Similar issues

More Dart issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.