# Security
> What is encrypted by what, what a paired peer can do to you, and what is still open.
> Source: https://jvoltci.github.io/sparsh/docs/security/

The design goal: **the only thing leaving your device unencrypted is what your
operating system already sees.**

Your files meet that goal. They ride a DTLS handshake the two devices perform
directly, and no relay ever holds a key for it. Your signalling meets it between
devices that have paired. It does not meet it when somebody types a six-digit
code, and that is said plainly below rather than buried.

## What is encrypted, by what

| Layer | Protects | By |
|---|---|---|
| HTTPS | the page itself | TLS 1.3 |
| Signalling | the SDP offer, answer and ICE candidates | AES-256-GCM over the relay's own TLS, and between paired devices the SDP is signed |
| Data channel | files, messages, MCP | DTLS, fingerprint pinned in the SDP |
| Media | audio, video, screen | SRTP from the DTLS handshake |

DTLS keys are never sent anywhere. The peers exchange certificate fingerprints
inside the SDP and then perform a fresh handshake directly between themselves. A
relay that drops or reorders events can break a connection. It cannot read one.

## Between paired devices, the SDP is signed

Every device holds a secp256k1 keypair and its public key **is** its identity.
Once two devices have paired, each knows the other's key, so the SDP carrying
the DTLS fingerprint is signed by the sender and verified by the receiver before
WebRTC is ever given it.

That matters most on the signalling server path, where the SDP is not encrypted
end to end. The server can still read an offer, which it always could. It can no
longer substitute one: swapping the fingerprint to sit invisibly in the middle
now fails verification and the connection is refused.

Proven rather than asserted. The end-to-end test was run with the check removed,
and the tampered SDP was accepted and the file sent. With the check in place the
same run is refused.

## First contact without a code

`sparsh invite` prints a link carrying **128 bits of secret and this device's
public key**, both in the URL fragment, which RFC 3986 keeps off the request
line. Neither reaches a server or an access log.

That closes first contact in both directions. The joiner verifies the host's
offer against the key it was handed, and the host verifies the joiner through a
proof of the secret bound to the joiner's own key, so a proof captured in flight
cannot be replayed by a different device. The invitation is single use.

An invitation offered over a transport that cannot check it is **refused**, not
quietly downgraded. A link that looked strong while verifying nothing would be
worse than the code it replaces, because it would not admit what it is.

## The typed code is still weak

A six-digit code is 900,000 values, and the relay topic is a plain hash of it.
An attacker computes all 900,000 hashes once, seconds of CPU, subscribes to every
one on the public relays, and waits. Any session that appears is matched straight
back to its code, and the code is the encryption key.

The attack is passive, precomputed and reusable. It needs no guessing and no race.

What it yields passively: the whole SDP, both peers' addresses and the DTLS
fingerprints. The file bytes stay unreadable, because DTLS performs its own key
exchange. What it yields actively: the file bytes, because whoever holds the code
can publish a forged answer and become the far end.

**Use the link.** It is why the link exists.

## What exposing an MCP server means

`sparsh expose <device> -- <command>` runs that command **as you, with your file
permissions**, whenever that one paired device opens a tools channel. The device
is authenticated, so the only question is whether you trust the agent on it with
that server. Choose what you expose the way you choose what you `sudo`: a
filesystem server rooted at one directory, not a shell.

What bounds it: no pairing has the grant by default; the grant names one server;
the server starts with a minimal environment rather than yours, so an API key in
your shell does not travel; one channel per session; a 10 MiB cap per message;
one audit line per call with the peer, the tool and the size, never the
arguments; and `unexpose` closes a live channel immediately.

Toward the device consuming tools, two requests never cross:
`sampling/createMessage`, so a remote server cannot spend your model, and
`roots/list`, so it cannot read your project layout.

## What is not solved

Said plainly, because a list of mitigations with the gaps left out is worse than
no list.

- **There is no accept prompt for an incoming file.** A paired device can send
  and it starts arriving. The consent was the pairing.
- **The peer chooses the filename**, and it reaches your downloads. The name is
  checked for path separators, not for honesty.
- **A remote server's replies are text in your agent's context.** The transport
  authenticates the device, not the intentions of the software on it.
- **None of this decides who you pair with.** It bounds the damage.
