Skip to content

Build with PsstPsst

Nearby file transfer

Implement PsstPsst Nearby file transfer: kind-15 offers, Blossom manifests, resumable transfers, binary records, flow control, and file integrity checks.

Protocol source ↗
On this page

draft optional

Abstract

This document specifies the FILE_TRANSFER extension to Nearby Messaging version 1. It sends plaintext file bytes through an authenticated Noise session. The associated NIP-17 kind-15 rumor describes both the direct plaintext and its encrypted Blossom representation.

Peers negotiate this extension with capability bit 2.

File identities

One attachment has two hashes:

Name Definition Use
ox SHA-256 of plaintext Direct-transfer identity
x SHA-256 of `ciphertext

The Blossom representation is AES-256-GCM(plaintext) with a 32-byte key, 12-byte nonce, and 16-byte authentication tag. Direct Nearby records carry plaintext inside Noise encryption. BLE itself is not a confidentiality boundary.

Kind-15 offer

The kind-15 rumor is both the attachment offer and the authorization grant. Its content is a BUD-10 URI:

blossom:<x>.bin?xs=<server-1>&xs=<server-2>...

The unsigned rumor has exactly one p recipient and exactly one of each required tag:

{
  "id": "<rumor-id>",
  "pubkey": "<sender-proximity-pubkey>",
  "created_at": "<unix-timestamp>",
  "kind": 15,
  "tags": [
    ["p", "<recipient-proximity-pubkey>"],
    ["file-type", "<plaintext MIME type>"],
    ["encryption-algorithm", "aes-gcm"],
    ["decryption-key", "<32-byte lowercase hex key>"],
    ["decryption-nonce", "<12-byte lowercase hex nonce>"],
    ["x", "<32-byte lowercase hex ciphertext hash>"],
    ["ox", "<32-byte lowercase hex plaintext hash>"],
    ["size", "<ciphertext byte length>"],
    ["plain-size", "<plaintext byte length>"]
  ],
  "content": "blossom:<x>.bin?xs=<server-1>&xs=<server-2>..."
}

size MUST equal plain-size + 16. The URI hash MUST equal the x tag, the extension MUST be .bin, and at least one valid xs server hint is required. Each xs identifies a server intended to hold that exact ciphertext.

BUD-10 as and sz parameters are optional. When present, as is only a discovery hint and sz MUST equal the authenticated size tag. Unknown URI parameters are ignored.

An HTTP(S) URL or an invalid blossom: manifest does not authorize direct file transfer. Existing optional kind-15 tags such as name, dim, thumbhash, duration, waveform, reply, and subject keep their meanings.

Any ciphertext interpreted from the manifest MUST hash to x. After decryption, the plaintext MUST match plain-size and ox. These integrity requirements do not prescribe when or whether a client retrieves the Blossom representation.

Records

All records use the Nearby secure-record envelope and network byte order. u64 values MUST NOT exceed 2^53 - 1.

Type Name Direction
0x30 FILE_REQUEST Receiver to sender
0x31 FILE_ACCEPT Sender to receiver
0x32 FILE_CHUNK Sender to receiver
0x33 FILE_PROGRESS Receiver to sender
0x34 FILE_COMPLETE Receiver to sender
0x35 FILE_CANCEL Either direction
0x36 FILE_REMOTE_AVAILABLE Sender to receiver

FILE_REQUEST

transferId          bytes[16]
rumorId             bytes[32]
ox                  bytes[32]
resumeOffset        u64
desiredChunkSize    u32
desiredWindow       u16
reserved            u16 = 0

transferId is random for one request attempt. resumeOffset is the first plaintext byte requested and may resume a contiguous partial transfer. Chunk size is 4–32 KiB and window size is one to four chunks. A new connection uses a new transfer ID.

Repeating the same active transfer ID and fields is idempotent. Reusing an active ID with different fields is invalid.

FILE_ACCEPT

transferId          bytes[16]
ox                  bytes[32]
plainSize           u64
acceptedOffset      u64
chunkSize           u32
windowChunks        u16
reserved            u16 = 0

The sender MAY reduce the requested chunk or window size. acceptedOffset is the requested offset or zero. The receiver discards its partial plaintext when zero is returned. The offset MUST NOT exceed plainSize.

FILE_CHUNK

transferId          bytes[16]
offset              u64
dataLength          u32
data                bytes[dataLength]

Chunks are contiguous and ordered. dataLength is non-zero, no larger than the accepted chunk size, and MUST NOT extend beyond plainSize. An unexpected offset invalidates the transfer.

FILE_PROGRESS

transferId          bytes[16]
receivedThrough     u64

receivedThrough is the first plaintext offset not yet accepted. It is cumulative, never decreases, and MUST NOT exceed plainSize. The sender keeps no more than windowChunks unacknowledged chunks in flight.

FILE_COMPLETE

transferId          bytes[16]
ox                  bytes[32]
plainSize           u64
status              u8
reserved            bytes[7] = 0

Status 0 is STORED; status 1 is ALREADY_PRESENT. A receiver sends STORED only after verifying the complete plaintext length and ox hash.

FILE_CANCEL

transferId          bytes[16]
reason              u16
retryable           u8
reserved            u8 = 0
Value Name Meaning
0x0001 NOT_AVAILABLE Missing, unauthorized, deleted, or unavailable
0x0002 INVALID_REQUEST Invalid field, offset, state, or negotiation
0x0003 BUSY Transfer capacity is occupied
0x0004 INSUFFICIENT_STORAGE Receiver cannot reserve space
0x0005 INTEGRITY_FAILURE Final size or ox mismatch
0x0006 USER_CANCELLED Local user cancelled
0x0007 TIMEOUT No progress before the peer’s deadline

Senders MUST use NOT_AVAILABLE for both existence and authorization failures so the response does not become a file-existence oracle.

FILE_REMOTE_AVAILABLE

rumorId             bytes[32]
x                   bytes[32]

This advisory tells the receiver that one planned Blossom location now has the ciphertext. The receiver MUST verify both fields against its stored offer. The record does not add a server location or relax hash verification. A receiver MAY ignore the advisory; the protocol does not prescribe source selection.

Direct exchange

Receiver                                      Sender
    |  FILE_REQUEST(rumorId, ox, offset)          |
    |-------------------------------------------->|
    |  FILE_ACCEPT(offset, chunk, window)         |
    |<--------------------------------------------|
    |  FILE_CHUNK(offset, data)                   |
    |<--------------------------------------------|
    |  FILE_PROGRESS(receivedThrough)             |
    |-------------------------------------------->|
    |                    ...                      |
    |  FILE_COMPLETE(ox, plainSize, STORED)       |
    |-------------------------------------------->|

A sender serves a request only when the Noise session is authenticated and accepted, the requester is the rumor’s sole p recipient, the sender authored that kind-15 rumor with the authenticated proximity identity, rumorId and ox match, and the exact verified plaintext is available.

After interruption, the receiver starts a new transfer ID and supplies its contiguous plaintext offset. Completed and cancelled transfer IDs are not reused.

Security

The message grant, not knowledge of ox, authorizes plaintext access. Noise protects direct plaintext in transit; x, ox, and size checks remain mandatory even after authenticated transport.