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
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>
134 lines
4.0 KiB
Go
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})
|
|
}
|