Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ through a browser store and has not been submitted. No surface is stable.

## [Unreleased]

### Removed

- **Annotations, entirely.** The daemon's `annot` module, the `/_control/annotations` routes,
the extension's content script, and the two npm dependencies it pulled in. souta's call, and
the right one: a site that wants notes should have notes built into it, and this product's
job is to let you see that site working.
What it bought is a property rather than a saving. **The daemon has no write path to a
remote** — not "writes are restricted", absent. The SFTP requests it can issue are `OPEN`,
`CLOSE`, `READ`, `OPENDIR`, `READDIR` and `REALPATH`, and the only open flag it defines is
`FXF_READ`; `RemoteFs` has no `append` or `mkdirs` to call.
The extension lost its only content script with it, so it no longer runs in, reads from, or
modifies the sites it opens, and it dropped the `scripting` permission and the
`http://*/*` optional host permission. It now asks for `proxy`, `storage` and
`http://127.0.0.1/*` and nothing else. The bundle went from ~152 KB to ~14 KB, of which the
138 KB content script was parsed on every page load.
Also gone with it: `--author` and `[server] author`, which existed only to name a log;
`Entry.owner` and the `longname` owner parse, which existed only to check one; and
`SECURITY.md`'s two sections about what was written and who a log belonged to.

### Fixed

- The extension was never attached to a release automatically. `release-assets.yml`
Expand Down
36 changes: 12 additions & 24 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,23 @@ In `chrome.storage.local`, which is local to your browser profile:
| aliases | the alias names the daemon reported, e.g. `docs`, `cluster` |

The control token is the piece worth singling out. It lives in the extension's service worker
and is never handed to a content script, because a page served from an alias origin is code
from the remote host and the extension treats it as untrusted. That is why annotating asks the
worker to act rather than acting inside the page.
and nothing else ever holds it, because a page served from an alias origin is code from the
remote host and the extension treats it as untrusted.

## What it reads

On a page under your configured suffix, and on no other page:

- **the page's URL**, which is how the extension knows which document a note belongs to
- **the page's text**, when it places a note — anchoring means finding the quoted words again,
which cannot be done without reading them

Both stay in the browser except as described next.
Nothing on any page. The extension has no content script: it does not run in, read from, or
modify the sites it helps you open. What loads is the site, unchanged.

## Where anything goes

To `http://127.0.0.1:<your port>`, and nowhere else. That is your own daemon, on your own
machine. It passes what it is given to the SSH host you configured, over your own SSH
connection, as the account you already had.

A note you write is therefore stored on your host, in a file beside the document, under your
own name. Nobody else receives it and nobody else can, because there is no intermediary to
receive it.
Nothing is written to your host. The daemon has no write path: the SFTP requests it can issue
are `OPEN`, `CLOSE`, `READ`, `OPENDIR`, `READDIR` and `REALPATH`, and the only open flag it
defines is read.

You can check rather than take this on trust: `extension/src/background.ts` contains exactly
one `fetch`, in `callDaemon`, and its URL begins `http://127.0.0.1:`.
Expand All @@ -60,20 +54,14 @@ one `fetch`, in `callDaemon`, and its URL begins `http://127.0.0.1:`.
it. Without it you would configure the browser's proxy by hand instead.
- **`storage`** — remembers the four things above, so you do not retype a 64-character token
on every page.
- **`scripting`** — registers the annotation script at runtime, for your configured suffix
only. At runtime precisely so the extension need not declare a match pattern covering every
site: the suffix is yours to choose, and anything declared in advance would have to be broad
enough for any choice you might make.
- **`http://127.0.0.1/*`** — talking to your daemon.
- **`http://*/*`, optional, requested when you click Connect** — the narrowest pattern that
can be *declared* ahead of time, because your suffix is not known until you connect. What is
actually *requested* is `http://*.<your suffix>/*`, and that is what the browser asks you to
approve. Declining leaves reading working and annotation off.
- **`http://127.0.0.1/*`** — talking to your daemon. It is the only host the extension is
allowed to reach, and there is no optional permission to grant later: it never asks to run
on the sites you open.

## Removing it

Uninstalling deletes everything in the table above with it. Notes already written are files on
your own host and stay there; delete them as you would any other file.
Uninstalling deletes everything in the table above with it. Nothing of yours is left behind
anywhere else, because nothing of yours was written anywhere else.

## Questions

Expand Down
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,14 @@ cached, so a seek does not pull the whole file and does not evict the page bodie
make revisits free. `If-Range` is never honoured, because the only validator on offer
is weak and the whole representation is the specified answer to that.

Annotations are per-author append-only logs beside the document, so two people annotating
one page write to different files and neither can lose the other's work. They are reachable
from the control API directly,

```
curl -H "x-ssh-browser-token: $TOKEN" -X POST --data '{"doc":"docs/index.html","op":"add","body":"a note"}' http://127.0.0.1:7391/_control/annotations
```

and the extension draws them. Highlights use the CSS Custom Highlight API and the panel is a
closed shadow root, so the document is never modified — no wrapper elements, no `<style>`
node, nothing a page script can see by walking the DOM.
**Nothing is written to your remote.** Not restricted, absent: the SFTP requests this can
issue are `OPEN`, `CLOSE`, `READ`, `OPENDIR`, `READDIR` and `REALPATH`, and the only open
flag it defines is `FXF_READ`. Nothing is injected into a page either — the extension has no
content script, so what you see is the site, unchanged.

There used to be an annotation feature here, writing per-author logs into a sidecar
directory. It is gone. A site that wants notes should have notes built into it; this is for
seeing that site work.

Four things are said rather than hidden, because each one means somebody's note is not
saying what it appears to say:
Expand Down
71 changes: 15 additions & 56 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ consume an interactive credential.

## The control API

Writes will go through a control API. It exists now, even though nothing writes yet,
because the boundary is easier to get right before there is something behind it than
after. Three things keep it away from the pages this daemon serves.
The control API is how the extension opens a host, stops one, and chooses a theme. It
writes nothing to your remote — see **Nothing is ever written** — but it does have effects,
and three things keep it away from the pages this daemon serves.

**It is routed only for requests whose Host is the loopback listener.** That is a
different classification from an alias request, so a page served under an alias origin
Expand Down Expand Up @@ -117,29 +117,21 @@ means underneath any page already open in it, which is the one thing an origin m
The comparison resolves `~` first, so `~/work` and `/home/me/work` are recognised as the
same place rather than refused as different ones.

## What the control API writes
## Nothing is ever written

Exactly one shape of path:
This daemon has no write path to your remote. Not "writes are restricted to one shape of
path" — there is no code that writes.

```
<dir>/.ssh-browser/<filename>/ann/<author>.jsonl
```
That is checkable rather than promised. The SFTP requests it can issue are `OPEN`, `CLOSE`,
`READ`, `OPENDIR`, `READDIR` and `REALPATH`, and the only open flag it defines is
`FXF_READ`. `WRITE` and `MKDIR` are not in the wire module; `RemoteFs` has no `append` or
`mkdirs` to call.

Nothing else. The path is computed from the document rather than taken from the request,
so a caller cannot name a destination at all. The document is resolved through the same
guards the read path uses — normalised after percent-decoding, and refused if any
component is a symlink — and the symlink rule matters more here than on the read side,
because a write that reached through a symlinked directory could place a file outside the
alias base entirely.

**The author is this daemon's configuration, not the request's.** A caller cannot write as
somebody else however it words the request. The id of a new annotation is minted by the
daemon and carries the author within it, and the timestamp is the daemon's clock: a caller
able to choose either could name someone else or reorder their log.

Writes are appends. A log has exactly one writer by construction, which is the only
arrangement that is safe without a lock, and it is why the format is per-author logs
rather than one shared file.
It reached that state by losing a feature. Annotations used to be written into a sidecar
directory beside each document, with per-author append-only logs and an ownership check on
them. They are gone, because they were the wrong thing for this product to own: a site that
wants notes should have notes built into it, and this daemon's job is to let you see that
site working.

## What is never served

Expand All @@ -166,36 +158,3 @@ through the control API, which does not come this way.

This is not a permission system. Everything else under the base is readable by anything else
under the base, and that is what an origin means.

## Who a log really belongs to

A log's filename is the only thing naming its author, so the filename is checked against
the file's owner. Two different problems show up in the same place.

The first is a misconfiguration: this daemon is set to write as `souta` while the SSH
account it reaches the host as is somebody else. Every note it writes then carries a name
nobody can support.

The second is a forgery. On a group-writable annotation directory, whoever gets there
first creates the file. If bob creates `alice.jsonl`, he can write records whose ids begin
`alice:` — and those ids are exactly what the merge accepts as alice's. POSIX permissions
are what should prevent this; the check is what notices when they did not.

The owner comes from the `longname` field of a listing, which is the only place SFTP v3
carries an owner's *name*. The numeric `uid` in the attrs cannot answer the question at
all: turning a number into an account name needs a passwd lookup, and the transport never
runs a shell. The parse declines anything that is not clearly a listing line, and a remote
that prints a number where a name should go is reported as unchecked rather than as a
mismatch — reading it as a mismatch would accuse whoever the number belongs to.

Three outcomes, and **"not checked" is kept apart from "checked and fine"**. The check is
worthless if a reader cannot tell those apart, and a daemon talking to a server that
reports owners differently would otherwise look as though it had verified something.

It costs nothing. The owner arrives on the listing a load already performs, so there is no
faster variant of this that works by skipping it.

**This is detection, not prevention.** By the time a mismatch is visible the records are
already written, and nothing here deletes them: hiding somebody's annotations on the
strength of a parsed `ls -l` line would be the worse failure. The mismatch is shown beside
the note, naming both the author the log claims and the account that owns the file.
Loading