package keyserver import ( "context" "encoding/json" "slices" "github.com/dariuszjaniak/warp/server/internal/federation" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/storage" ) // The writing side of device list tracking: telling everyone who shares a // room with a user that the user's keys changed. // // Locally that is a wake-up. The change is already in the device list // stream, moved in the same transaction as the keys themselves, and every // sync works out from the stream whose changes concern it; all a sync // blocked in a long poll needs is to be told to look. Remotely it is an // EDU to every server with a user in one of those rooms, which is how a // server that has the user's keys cached knows to drop them. // sharers is who shares a room with a user, the user included. func (k *API) sharers(ctx context.Context, userID string) []string { out := []string{userID} if k.rooms == nil { return out } users, err := k.rooms.UsersSharingRooms(ctx, userID) if err != nil { // A missed wake-up costs a client one sync timeout, and a missed // EDU costs a remote server one stale key until the next change; // neither is worth failing the upload that caused it. k.log.Warn("could not read who shares a room with a user", "user_id", userID, "error", err) return out } for _, u := range users { if !slices.Contains(out, u) { out = append(out, u) } } return out } // changed reacts to a move of a user's device list: it wakes the local // syncs it concerns and returns the other servers that have to hear of // it. func (k *API) changed(ctx context.Context, userID string) []string { users := k.sharers(ctx, userID) var ( local []string servers []string ) for _, u := range users { server, err := id.ServerNameOf(u) switch { case err != nil: continue case server == k.serverName: local = append(local, u) case !slices.Contains(servers, server): servers = append(servers, server) } } if k.notifier != nil { k.notifier.NotifyUsers(local) } slices.Sort(servers) return servers } // federating reports whether there is a running queue to send EDUs to. func (k *API) federating() bool { return k.outbound != nil && k.outbound.Running() } // announceDevices tells everyone about moves of a local user's devices: // keys uploaded, changed or deleted. keys holds the current keys of the // devices that still have them; a device missing from it is announced as // deleted. func (k *API) announceDevices(ctx context.Context, userID string, updates []storage.DeviceListUpdate, keys map[string]json.RawMessage) { if len(updates) == 0 { return } servers := k.changed(ctx, userID) if len(servers) == 0 || !k.federating() { return } for _, u := range updates { content := federation.DeviceListUpdateContent{ UserID: userID, DeviceID: u.DeviceID, StreamID: u.StreamPos, } if u.PrevPos > 0 { content.PrevID = []int64{u.PrevPos} } if raw, ok := keys[u.DeviceID]; ok { content.Keys = raw } else { content.Deleted = true } encoded, err := json.Marshal(content) if err != nil { continue } k.outbound.QueueEDU(ctx, servers, federation.EDU{Type: federation.EDUDeviceListUp, Content: encoded}) } } // announceSigningKeys tells everyone a local user's cross-signing keys // changed: the master key, or a signature on it, or the self-signing key. // The user-signing key is not announced — nobody but its owner ever sees // it. func (k *API) announceSigningKeys(ctx context.Context, userID string) { servers := k.changed(ctx, userID) if len(servers) == 0 || !k.federating() { return } master, selfSigning, _, err := k.crossSigningKeys(ctx, userID, "") if err != nil { k.log.Warn("could not read cross-signing keys to announce them", "user_id", userID, "error", err) return } encoded, err := json.Marshal(federation.SigningKeyUpdateContent{ UserID: userID, MasterKey: master, SelfSigningKey: selfSigning, }) if err != nil { return } k.outbound.QueueEDU(ctx, servers, federation.EDU{Type: federation.EDUSigningKeys, Content: encoded}) }