Files
Warp/server/internal/keyserver/devicelists.go
T
tajniak81andClaude Opus 5.5 74f7bc8805
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
keyserver: cache remote users' device and cross-signing keys
POST /keys/query for a remote user who shares a room with a local user is
now answered from a cache, fetched whole with GET /user/devices/{userId}
on a miss. Incoming m.device_list_update EDUs are applied when their
prev_id matches the cached stream_id; anything else marks the copy stale
and it is fetched again in the background, and a stale copy is never
served. m.signing_key_update replaces the cached cross-signing keys. A
user's copy is dropped once no local user shares a room with them, by a
keyserver listener on the roomserver's emit stage.

Migration 0009 adds keyserver_remote_users, keyserver_remote_device_keys
and keyserver_remote_cross_signing_keys for sqlite and postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:36:08 +02:00

134 lines
4.0 KiB
Go

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})
}