Encrypted room: no ghosts and no puppet on a fresh-store deploy (unencrypted works) #30
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#30
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
On a fresh-store deploy (v0.2.0-rc4
.debon the VPS, systemd service,/var/lib/nether-voicebridgewiped before first start), the encrypted bridge does not function in either direction:The unencrypted bridge (Light Voice) works normally on the same process — ghosts are assigned, post
m.call.member, mirror mute, etc.Important context
This is after the fresh-store E2EE auto-detection fix (
a6efd71, shipped in rc4), which madedetect_room_encryptionauthoritative viaRoom::latest_encryption_state(). So detection is no longer the problem — the room is now correctly detected as encrypted, but the encrypted media/membership path itself fails to come up on a fresh store.Pattern mirrors the detection bug it followed: the encrypted bridge works locally with an established store (verified earlier the same day) but fails on a freshly-provisioned store. Strongly suggests an encrypted-path bootstrap step that silently depends on pre-existing crypto/store state and doesn't establish itself on first run.
Environment
.deb, installed on VPS, run as thenether-voicebridgesystemd service./var/lib/nether-voicebridgewiped before start → genuine fresh crypto store (new per-bridge devices, new per-ghost crypto clients).@voicebridge_as, self-provisioned device).Evidence so far
Only unencrypted (Light Voice) logs were captured, and they are healthy:
Dark Voice (encrypted) logs were NOT yet captured — that's the first task.
Next steps (tomorrow)
Collect the encrypted bridge's logs from the VPS:
Confirm: (a) Dark resolved
detected_encrypted=true … Olm; (b) whether the subscriber posts its ownm.call.member(it MUST be a visible member in E2EE); (c) whether ghosts post membership in the encrypted room; (d) any crypto/Olm/SQLite-store errors during fresh-device provisioning; (e) key-distribution warnings (reached NO recipient devices,unreachable).Reproduce locally with a FRESH store (
NVB_DATA_DIR=$(mktemp -d)) and actually join the encrypted call. The earlier fresh-store test only checked the detection log line, not live call participation — so the bug likely reproduces locally this way. (Do NOT run a local instance while the VPS instance is live — both would post ghosts/subscriber to the same rooms and conflict.)Hypotheses to check (see also the encrypted-room E2EE findings in repo history / memory):
m.call.membernot posting on a fresh crypto device → invisible in Element Call (EC builds its roster from membership, not LiveKit presence).org.matrix.msc3401.call.membernot at 0 on the VPS's encrypted room (verify the RAWm.room.power_levels— Element's friendly UI label is unreliable). A wrong PL silently blocks ghost/subscriber membership → exactly this symptom.Severity
Blocks encrypted-room bridging on any fresh/new deployment — i.e., every new install. Gates the final v0.2.0 release. Unencrypted bridging is unaffected.
Root cause found and fixed (live-verified on Tuwunel; commit
84b0a0c).The "no ghosts / no puppet" framing was partly off: on a fresh store the encrypted bridge comes up fine and Discord→Matrix works. The real failure is receive-only — Discord users can't hear Matrix speakers.
The bot's
device_idis derived from the bridge name, not the crypto store, so a wiped store re-registers the samedevice_idwith new identity keys. Peers that cached the old identity reject the change (SigningKeyChanged) and never deliver the per-call media key (io.element.call.encryption_keys) to the subscriber → it can't decrypt the human's frames (LiveKitE2eeStatestuck atMissingKey→ −96 dB silence). The send direction works because there the bridge encrypts to the human's own stable devices — hence "I can hear them, they can't hear me."Fix (
84b0a0c): pair the device identity with the store — persist a<store_path>/device_idmarker. A genuinely fresh store mints a unique id (peers see a brand-new device and open Olm normally); a pre-existing store adopts its configured id (back-compat, no re-key); a marker present is reused on restart. Appservice path only (theaccess_tokenpath keeps its token-pinned device).Verified live: fresh store →
@darktrackMissingKey→Ok, media key received, clear audio both directions; warm store adoptsdark-voiceunchanged. Build + new resolver unit tests + clippy all green.Non-blocking follow-ups (candidates for separate issues): the superseded device lingers on the homeserver after each wipe (device-list growth, cosmetic); the per-ghost crypto clients use the same reuse pattern (harmless — ghosts are senders — but it's the source of the
SigningKeyChangedlog churn).