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.
This commit is contained in:
@@ -13,8 +13,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -142,30 +142,45 @@ func New(cfg config.MatrixConfig) (*Client, error) {
|
|||||||
"room", evt.RoomID, "event_id", evt.ID, "sender", evt.Sender, "err", err)
|
"room", evt.RoomID, "event_id", evt.ID, "sender", evt.Sender, "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginAs enables the cryptohelper to re-login if the token expires
|
// IMPORTANT: do NOT set ch.LoginAs here. We already performed an explicit
|
||||||
ch.LoginAs = &mautrix.ReqLogin{
|
// password login above (or restored a saved token), so mx is fully
|
||||||
Type: mautrix.AuthTypePassword,
|
// authenticated against device.json's device. When LoginAs is set AND the
|
||||||
Identifier: mautrix.UserIdentifier{
|
// crypto store is fresh, cryptohelper.Init() performs a SECOND login
|
||||||
Type: mautrix.IdentifierTypeUser,
|
// (StoreCredentials=true), minting a separate device and pinning the olm
|
||||||
User: cfg.UserID,
|
// account to it — while device.json still records the first device. That
|
||||||
},
|
// split-brain is exactly the "device never verifies" bug: the device the
|
||||||
Password: cfg.Password,
|
// token authenticates as is not the device the crypto account belongs to,
|
||||||
InitialDeviceDisplayName: cfg.DisplayName,
|
// 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 {
|
if err := ch.Init(context.Background()); err != nil {
|
||||||
return nil, fmt.Errorf("crypto helper init: %w", err)
|
return nil, fmt.Errorf("crypto helper init: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mx.Crypto = ch
|
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()
|
mach := ch.Machine()
|
||||||
existingKeys, err := mach.GetOwnCrossSigningPublicKeys(context.Background())
|
hasKeys, deviceCrossSigned, err := mach.GetOwnVerificationStatus(context.Background())
|
||||||
if err != nil {
|
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 !hasKeys || !deviceCrossSigned {
|
||||||
|
if hasKeys {
|
||||||
|
slog.Warn("cross-signing: keys exist but current device is not cross-signed, resetting cross-signing")
|
||||||
}
|
}
|
||||||
if existingKeys == nil || existingKeys.MasterKey == "" {
|
|
||||||
_, _, err = mach.GenerateAndUploadCrossSigningKeys(context.Background(), func(ui *mautrix.RespUserInteractive) interface{} {
|
_, _, err = mach.GenerateAndUploadCrossSigningKeys(context.Background(), func(ui *mautrix.RespUserInteractive) interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"type": mautrix.AuthTypePassword,
|
"type": mautrix.AuthTypePassword,
|
||||||
@@ -195,7 +210,7 @@ func New(cfg config.MatrixConfig) (*Client, error) {
|
|||||||
slog.Info("cross-signing: master key signed")
|
slog.Info("cross-signing: master key signed")
|
||||||
}
|
}
|
||||||
} else {
|
} 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",
|
slog.Info("E2EE initialized",
|
||||||
|
|||||||
Reference in New Issue
Block a user