<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git, branch 0.2</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.2</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.2'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-14T18:54:47Z</updated>
<entry>
<title>chore: release 0.2.0</title>
<updated>2026-08-14T18:54:47Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T18:54:47Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e525f7e123b621deec10a9719efa10ccafa93ad0'/>
<id>urn:sha1:e525f7e123b621deec10a9719efa10ccafa93ad0</id>
<content type='text'>
All three packages together, as the conventions require, plus the RPM and DEB
metadata and their changelogs.

The tag said 0.2 while every package announced 0.1.0, which would have shipped an
RPM claiming to be the reviewed build while containing a different protocol: the
hub schema lost the user identity keys, tokens lost pk_user, and
gek_bundle_store left the wire. Pre-1.0, a breaking change bumps MINOR.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>merge: Phase 11.5 security remediation, invite redesign, per-node identity</title>
<updated>2026-08-14T17:35:37Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T17:35:37Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c83a4f6ab0c8a83e8679e78427ae60dc29bb2c60'/>
<id>urn:sha1:c83a4f6ab0c8a83e8679e78427ae60dc29bb2c60</id>
<content type='text'>
Brings in the security remediation branch. Three bodies of work, and what they
changed about what this project may claim.

Phase 11.5 closed the gap between the documents and the code: the unauthenticated
node HTTP API and the TCP transport deleted, one handshake shared by the
remaining two transports, mutual authentication, structured admin transcripts,
upload confinement, group isolation, revocation that reaches nodes. Six critical
and seven high findings closed, bounded, or deferred by decision.

The invite redesign closed H3 and M3 — the last open High. The hub was the key
directory: an inviter fetched the invitee's key from it and wrapped the group key
for whatever came back, so a hub answering with its own key was handed the group
key by an honest member following the protocol exactly. That lookup is gone. The
node holds the group key and wraps it itself, for a key its recipient proves
possession of, bound to an account by a one-time code the hub never sees. M3 fell
out of the same work: node authority comes from a local roster, never from the
hub.

Per-node identity cut what remains of C4 down to one operator. A single keypair
used to be copied to every node its owner joined; each node now gets its own, so
cracking the bundle on one machine yields a key that is a stranger everywhere
else — and on that machine, one that unlocks nothing its holder did not already
serve. The bundle KDF moved to Argon2id 128 MB, and the hub stopped storing or
publishing user keys at all.

What this project may now say: the hub cannot read your content unless it ships
you malicious client code. T3 remains, accepted (D1), and is what the native
client removes. C4 is reduced, not closed, until 13.3. Chat is still plaintext at
rest until Phase 15. Draft-v5 §2 states each claim against the adversary it holds
against, which is the convention this branch exists to keep.

Four defects were found by deploying it and using a browser, none by the test
suite: a node going deaf on its hub socket, a token that predated group
membership, a client reading values before they were assigned, and identity keys
a browser held but never re-read. The lessons are recorded in CLAUDE.md.

Tests: 343 across the three packages, plus QE/deploy/e2e.py — register, pair,
invite, join, download, stream, second browser, revoke — run against the live
deployment on a wiped hub and node.
</content>
</entry>
<entry>
<title>fix(node): an operator keeps their role when reconnecting</title>
<updated>2026-08-14T17:25:39Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T17:25:39Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d324792d68503109ab99616af6c85ee37045e169'/>
<id>urn:sha1:d324792d68503109ab99616af6c85ee37045e169</id>
<content type='text'>
An operator's roster row is node-wide, so looking it up by the group they happen
to be opening found nothing and the client was told it had no role on a node it
administers. Falls back to the node-wide row.

Surfaced by running the live workflow twice: the first pass pins, the second is
recognised — and only the second exercised this path.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: user guide and conventions catch up with per-node identity</title>
<updated>2026-08-14T17:15:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T17:15:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cfe49407abb3eb48b996ab6d08d66985f29846f0'/>
<id>urn:sha1:cfe49407abb3eb48b996ab6d08d66985f29846f0</id>
<content type='text'>
USERGUIDE said registration submits your public keys "so other members can wrap
GEK bundles for you". Both halves are wrong now: registration creates an account
and nothing else, and nobody wraps anything for a key fetched from the hub. The
API reference and the register body followed the same correction.

CLAUDE.md gains the block a future session needs before touching registration or
anything shaped like a user's public key: keys are born at first contact with a
node and stay there, the hub publishes none, tokens carry no pk_user, and a
scripted signup is now a real account.

Left alone deliberately: first-review.md, docs/poc-v1*.md and poc/spike-results.md
still describe the old JWT and registration. They are records of what was true on
their date, like second-review's verdict table, and draft-v5 is what states the
present.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat!: identity keys per node — C4's blast radius drops to one operator</title>
<updated>2026-08-14T15:51:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T15:51:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f0984e86d9cb596a282ce6feb7cfc2f075b2794b'/>
<id>urn:sha1:f0984e86d9cb596a282ce6feb7cfc2f075b2794b</id>
<content type='text'>
One keypair was copied to every node its owner joined, so cracking the bundle on
any single node yielded the identity used on all of them: their content on other
operators' machines, and the ability to sign as them anywhere. That lateral reach
was the part of C4 worth attacking.

Each node now gets its own keypair, generated the first time its owner joins it
and left with that node alone. An operator who cracks what sits on their own disk
holds a key that is a stranger to every other node — and on their own node, one
that unlocks nothing they did not already hold: they serve the content, the index
and every byte of it by design.

Nothing changes for the user. A first contact with a node already needed that
operator's code, and the key is created in the same step; a second browser still
recovers it from the node with the passphrase alone. Two operators can also no
longer tell they host the same person by comparing keys.

BREAKING, and deliberately without a compatibility path — the deployment is wiped
for the next demo:

  - users.pk_ed25519 / pk_x25519 dropped (migration a7c31f9e40b2)
  - registration no longer sends or stores a key
  - PUT /v1/users/me/keys and regenerateKeys() gone; rotation is now
    `member unpin` plus a fresh code, decided on the machine that pinned it
  - /pubkeys returns an account id and a node's linking key. It was the directory
    H3 read, and nothing wraps for it any more
  - the pk_user JWT claim is gone

That last one closed a live defect the inventory turned up: the node recorded
pk_user as the uploader's identity and authorized deletion against it, so a hub
issuing a token naming its own key could delete anyone's uploads on any node.
Attribution now uses the key the node itself pinned.

A simplification falls out. Registration generates nothing, so a scripted signup
is a real account: `demo.py bootstrap` takes a wiped hub and node to a working
demo with no browser, which was impossible while keys were born in one.

Also fixes, found by running it on a wiped deployment: the key handed back on a
join now belongs to the group the connection is for, not the group named in the
invitation — an operator pairs node-wide but redeems the code while opening a
group, and expects to read it.

Tests: 343, including the two that state the property — a key pinned by one node
is refused at another, and someone else's code does not admit it. Verified end to
end against a wiped hub and node: bootstrap, pair, invite, join, download,
stream, second browser, revoke.

Design: docs/per-node-identity-v1.md

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: Argon2id, the multi-browser property, and what a browser found</title>
<updated>2026-08-14T12:43:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T12:43:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9fa2117de1caf4d713cc0b7a310b9549467738c3'/>
<id>urn:sha1:9fa2117de1caf4d713cc0b7a310b9549467738c3</id>
<content type='text'>
draft-v5 §7 rewritten around the keypair bundle, because that is where the last
open finding actually lives. New §7.1 states the adversary (an operator holding
their own node's disk), what cracking a bundle yields (identity keys, hence
content on *other* nodes and the ability to sign as that user — not the content
they host in the clear by design), and the measured numbers rather than
adjectives: PBKDF2 241 ms vs Argon2id 88 ms natively, a GPU ceiling moving from
~8k to ~2k guesses/s, six days for a 10⁹ dictionary run, four random words
outlasting the sun. The honest summary is in there too — a factor of four on one
card, not a thousand; what it buys is the cost of scale.

§2 gains the row the table never had: **your identity keys stay yours**, ⚠️
against a malicious node operator. An operator hosts your content by design, and
that was documented; that they can also try to become *you* was not. That is the
difference between reading what they host and reading what other operators host.

§4 records that the challenge now carries `node_pk`, why (a first-time member
signs a transcript naming the node and has no GEK to complete a handshake with),
and that it is checked against the ack rather than trusted. Also that refusals
carry a code, and what `not_a_member` usually means.

§8.1 states the multi-browser property plainly — one identity across browsers,
recovered with the passphrase, no second code — together with its cost, since it
is the same mechanism as C4.

invite-pairing-v1 is no longer "a proposal": it shipped. §9bis gains the four
browser-found failures and their common thread — e2e.py is a second
implementation of the client, written in the right order by construction, so it
proves the protocol and nothing about app.js.

CLAUDE.md gets the two things a future session must not rediscover the hard way:
the KDF parameters live in three places held identical by a parity test, and an
unbounded await on the hub socket makes a node silently unreachable (three found).

second-review: C4 marked reduced, not closed. devel-phases-next: 12.2's CSP must
keep `wasm-unsafe-eval`, or the strict policy locks every user out of their keys.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>perf(client): bundle KDF to 128 MB, and derive it once per sign-in</title>
<updated>2026-08-14T12:25:44Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T12:25:44Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1c96aceb54d66cae1b48aa0eb8887f68e53f9e24'/>
<id>urn:sha1:1c96aceb54d66cae1b48aa0eb8887f68e53f9e24</id>
<content type='text'>
Argon2id memory 64 → 128 MB. Memory is the lever, not time: it caps how many
guesses a card can hold at once, so the ceiling on one high-end GPU moves from
roughly 4k to roughly 2k guesses/s and its 24 GB fits ~187 lanes instead of ~375.
Measured through the vendored build: 640 ms, against 322 ms at 64 MB.

While measuring the real cost of a sign-in, found the SPA deriving the bundle key
twice — once for the key pair kept for the session, then again inside
decryptBundle() for the local bundle. At these parameters that is 0.6 s of pure
waste. Measured now, end to end:

    auth_key   (PBKDF2 600k)     239 ms
    bundle v1  (PBKDF2 600k)     240 ms   legacy, until every bundle is upgraded
    bundle v2  (Argon2id 128MB)  650 ms
    -----------------------------------
    sign-in                    1 129 ms   (889 ms once no v1 bundles remain)

Once per sign-in, and only then: reopening a group, downloading, streaming and
reloading the page all reuse the key, which lives in IndexedDB from login.

Also bounds two waits in the node's hub WebSocket, found because the node went
silent again mid-deploy. It had reconnected after the hub restart, sent its auth
frame, and waited for a reply that never came — `ws.recv()` had no timeout, so a
hub that accepts a socket and then says nothing for a few seconds while starting
up parks the task forever: node running, logging nothing, invisible to everyone.
The auth exchange now times out at 15 s, connect at 15 s, and a refused auth
retries with a fresh token instead of ending the task for good.

QE harness signs in once per account and reuses the token — several clients there
stand for several browsers of one person, and what tells them apart is which keys
they hold, not which token, while the hub quite rightly rate-limits repeated
logins from one address.

Tests: 341, plus the live workflow.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): Argon2id for the keypair bundle, and remove the backup toggle</title>
<updated>2026-08-14T10:42:51Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T10:42:51Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=86563d5db0ae19fc58336b71a5c29a8712973590'/>
<id>urn:sha1:86563d5db0ae19fc58336b71a5c29a8712973590</id>
<content type='text'>
Two corrections to yesterday's judgement, in the order they matter.

**The toggle is gone.** Asked to make the remote key backup optional, I shipped a
setting whose "off" position meant: no second browser, ever, and clearing your
storage destroys the account. I wrote the warning that says so without drawing
the conclusion. A control whose only effect is to break the ordinary case is not
a control, and removing an exposure by removing the feature is not a fix. Every
browser backs its keys up again, unconditionally.

**The exposure is fixed where it actually lives: the KDF.** The keypair bundle
rests on every node whose group its owner joins, protected by the passphrase
alone (finding C4). It used PBKDF2-SHA512 at 600k — compute-only, which is
exactly what a GPU eats. Measured on this machine: PBKDF2 600k costs 241 ms and
Argon2id 64 MB/t=3 costs 322 ms, near enough the same honest work, except only
one of them forces an attacker to find 64 MB per guess.

So the bundle key is now Argon2id 64 MB / t=3 / p=1, via a vendored WebAssembly
build (no external host — the CSP forbids one, and 12.2 will tighten it further).
Parameters chosen by measurement through that build: 19 MB is OWASP's floor at
118 ms, 256 MB is 1.3 s and too slow for a phone, 64 MB sits where a login should.

What this buys, stated honestly: cracking a bundle yields the owner's identity
keys, and with them content on OTHER nodes and the ability to sign as them — not
the content on the operator's own node, which they host in the clear by design.
Argon2id raises that price steeply; it does not remove it, and a weak passphrase
still loses. Hence the floor raised to 12 characters and ~60 bits in the same
breath, which can only be enforced client-side: with the password split (T1) the
hub never sees a passphrase.

Migration is automatic and invisible. Bundles carry an "MBK2" marker; the old
form is still readable, and is re-encrypted the first time a browser backs it up.
Both keys are derived at sign-in, because which one a bundle needs is only known
once it is read and the passphrase is deliberately not kept around.

Two implementations of the KDF now exist — the browser's WASM and argon2-cffi in
QE — so a parity test holds them byte-identical. A disagreement would not look
like an error; it would look like an account nobody can open.

keypair_bundle_delete stays, without a UI. It is the mechanism behind withdrawing
your data from a node, exercised end to end, and it will belong to a deliberate
"forget me on this node" action rather than a setting that quietly disables
multi-device.

Verified against the live deployment: the full workflow passes, including
recovering keys on a second client from the passphrase alone.

Tests: 341.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): make the key backup a choice, and raise the passphrase floor</title>
<updated>2026-08-14T10:06:31Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T10:06:31Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2caa93dbc06161b5d3f776a204ac8d921df92126'/>
<id>urn:sha1:2caa93dbc06161b5d3f776a204ac8d921df92126</id>
<content type='text'>
Two things the multi-browser story made obvious.

**The backup is now opt-out.** Keys are kept, encrypted with the passphrase, on
every node whose group you join — that is what lets a second browser recover
them, and it is finding C4: a PBKDF2-protected blob on other people's disks,
attackable offline at the speed of PBKDF2, which is memory-light and therefore
cheap on a GPU. Until now everybody paid that cost, including people who will
only ever use one browser and get nothing back for it.

Settings → "Use this account on other devices". Turning it off does not merely
stop future uploads: the next connection to each node withdraws what that node
already holds (new keypair_bundle_delete, which only ever deletes the caller's
own, taken from the authenticated session and never from the message). The
warning says plainly what it costs — clearing the browser then loses everything
encrypted for that account, with no recovery, which is the point of choosing it.

Default is on. Silent, unrecoverable key loss is worse for an ordinary user than
an exposure the roadmap already tracks, but that is a judgement call and it is
now visible and reversible instead of implicit.

**Passphrase floor 8 → 12 characters, plus a strength estimate** shown while
typing, with a refusal below ~60 bits. This number matters more here than in
most applications: it is what stands between a node operator and your identity
keys. It has to live in the client — with the password split (T1) the hub never
sees a password and cannot enforce anything about one — so the UI says why it
is asking, rather than nagging.

The estimator is deliberately conservative and dependency-free: character
classes and length, penalised for repetition and for the handful of patterns
everyone tries.

Verified against the live deployment: withdrawing the backup leaves a second
browser unable to recover anything, which is exactly what it promises, and
re-enabling restores it.

Tests: 338.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>test: prove a second browser works after pairing</title>
<updated>2026-08-14T09:15:27Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T09:15:27Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6309894d019421f54cff311e630f3897f7eba93e'/>
<id>urn:sha1:6309894d019421f54cff311e630f3897f7eba93e</id>
<content type='text'>
The mechanism was already there — the encrypted keypair bundle goes to the node
after a first successful connection, and any client holding the password can
recover it — but nothing exercised it. e2e.py never pushed a bundle, so the case
that matters to an ordinary user was the one case never tested.

It now does what app.js does: backs the member's keys up to the node, then opens
a second client carrying nothing but a username and a password. Against the live
deployment that client recovers its identity keys, is recognised as the same
person with no second code, gets the same group key, and browses the group.

Also guards the ordering this depends on: the keypair bundle must be fetched
before joinGroup() runs, or a browser that did not register has no key to sign
the join with — invisible on the browser that did register, broken on every
other one.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
