From 7b76f9ed238813f076b273280c4b5cb71caec28c Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:35:29 -0700 Subject: [PATCH] Fix Matrix E2EE: stop double-login and make cross-signing bootstrap reachable Two bugs prevented Pete's device from ever cross-signing itself: 1. Double login / split-brain: New() did a manual mx.Login() AND set ch.LoginAs, so cryptohelper.Init() logged in a second time on a fresh crypto store, minting a separate device. device.json recorded one device while the olm account belonged to another, and every cold start leaked an orphan device. Pete's outgoing events were attributed to a device whose keys no client could verify. Removed ch.LoginAs (auth is already handled by the manual login + isTokenValid re-login path). 2. Unreachable bootstrap: the reset gate used IsDeviceTrusted(mach.OwnIdentity()), but OwnIdentity() hard-codes Trust=TrustStateVerified, so it always returns true and the bootstrap/reset branch was never entered. Replaced with GetOwnVerificationStatus(), which actually checks whether the current device key is signed by our self-signing key. --- internal/matrix/client.go | 49 +++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/internal/matrix/client.go b/internal/matrix/client.go index f045c43..0d4c01f 100644 --- a/internal/matrix/client.go +++ b/internal/matrix/client.go @@ -13,8 +13,8 @@ import ( "log/slog" "net/http" "os" - "strings" "path/filepath" + "strings" "sync" "time" @@ -142,30 +142,45 @@ func New(cfg config.MatrixConfig) (*Client, error) { "room", evt.RoomID, "event_id", evt.ID, "sender", evt.Sender, "err", err) } - // LoginAs enables the cryptohelper to re-login if the token expires - ch.LoginAs = &mautrix.ReqLogin{ - Type: mautrix.AuthTypePassword, - Identifier: mautrix.UserIdentifier{ - Type: mautrix.IdentifierTypeUser, - User: cfg.UserID, - }, - Password: cfg.Password, - InitialDeviceDisplayName: cfg.DisplayName, - } - + // IMPORTANT: do NOT set ch.LoginAs here. We already performed an explicit + // password login above (or restored a saved token), so mx is fully + // authenticated against device.json's device. When LoginAs is set AND the + // crypto store is fresh, cryptohelper.Init() performs a SECOND login + // (StoreCredentials=true), minting a separate device and pinning the olm + // account to it — while device.json still records the first device. That + // split-brain is exactly the "device never verifies" bug: the device the + // token authenticates as is not the device the crypto account belongs to, + // and each cold start leaks another orphan device. Token-expiry recovery is + // already handled by the isTokenValid() check + re-login path above. if err := ch.Init(context.Background()); err != nil { return nil, fmt.Errorf("crypto helper init: %w", err) } mx.Crypto = ch - // Bootstrap cross-signing only if not already set up. + // Ensure our CURRENT device is cross-signed. We bootstrap (generate + upload + // fresh cross-signing keys, then self-sign) whenever either no keys exist yet, + // or keys exist on the server but our device is not signed by them. The latter + // is exactly the post-wipe state: when crypto.db is deleted the private + // self-signing key is lost (it is never persisted locally without an SSSS + // passphrase, and we discard the random recovery key), so the lingering server + // master key is useless to us and must be replaced. + // + // CRITICAL: do NOT gate this on IsDeviceTrusted(mach.OwnIdentity()). + // OwnIdentity() hard-codes Trust=id.TrustStateVerified, and ResolveTrustContext + // short-circuits on that, so the call is a tautology that ALWAYS returns true — + // it makes this branch unreachable and the device never gets signed. + // GetOwnVerificationStatus does a real check: is our own device key signed by + // our self-signing key? mach := ch.Machine() - existingKeys, err := mach.GetOwnCrossSigningPublicKeys(context.Background()) + hasKeys, deviceCrossSigned, err := mach.GetOwnVerificationStatus(context.Background()) if err != nil { - slog.Warn("cross-signing: failed to fetch existing keys", "err", err) + slog.Warn("cross-signing: failed to check verification status", "err", err) } - if existingKeys == nil || existingKeys.MasterKey == "" { + if !hasKeys || !deviceCrossSigned { + if hasKeys { + slog.Warn("cross-signing: keys exist but current device is not cross-signed, resetting cross-signing") + } _, _, err = mach.GenerateAndUploadCrossSigningKeys(context.Background(), func(ui *mautrix.RespUserInteractive) interface{} { return map[string]interface{}{ "type": mautrix.AuthTypePassword, @@ -195,7 +210,7 @@ func New(cfg config.MatrixConfig) (*Client, error) { slog.Info("cross-signing: master key signed") } } else { - slog.Info("cross-signing: already configured, skipping bootstrap") + slog.Info("cross-signing: already configured and current device cross-signed, skipping bootstrap") } slog.Info("E2EE initialized",