// Package keyserver holds what end-to-end encryption needs a homeserver to // keep for its users' devices: their identity keys, their one-time and // fallback keys, the user's cross-signing keys and the signatures between // all of those, the server-side backup of room keys, and the device list // stream that tells other devices when any of it changed. // // None of it is something the server can use. Every key here is public, or // encrypted to a key the server never sees; what the server provides is a // place to publish them, the guarantee that a one-time key is handed out // once, and the bookkeeping of who has to be told about a change. That is // the whole of the design, and the reason the package is mostly checks: // the server cannot tell a good key from a bad one, but it can refuse a // key uploaded for somebody else's device, a cross-signing key that its // master key did not sign, and a signature that does not verify. // // The transport of encrypted messages between devices — to-device // messaging — is the syncapi's, because a device only ever receives one in // a /sync. So is the reporting of device list changes, for the same // reason: whose keys changed is one of the streams of a sync response. // What is here is the writing of that stream. // // Phase 7 of PLAN.md. package keyserver import ( "context" "errors" "fmt" "log/slog" "time" "github.com/dariuszjaniak/warp/server/internal/federation" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/storage" ) // Errors callers have to tell apart. Each is wrapped with the detail of // what was wrong, so a handler can answer with the right errcode and still // tell the client which key it was. var ( // ErrInvalidParam is a request that is well-formed JSON but not a // valid one: keys for another user's device, a key ID without an // algorithm, a cross-signing key with the wrong usage. ErrInvalidParam = errors.New("invalid parameter") // ErrMissingParam is a request that leaves out something it needs, // such as a self-signing key with no master key to have signed it. ErrMissingParam = errors.New("missing parameter") // ErrInvalidSignature is a signature that does not verify. ErrInvalidSignature = errors.New("invalid signature") // ErrKeyConflict is a one-time key uploaded under an ID that already // holds a different one. ErrKeyConflict = storage.ErrKeyConflict // ErrNoSuchBackup is a key backup version that does not exist. ErrNoSuchBackup = storage.ErrNoSuchBackup // ErrNoSuchBackupKey is a room key that is not in the backup. ErrNoSuchBackupKey = storage.ErrNoSuchBackupKey ) // WrongBackupVersionError is a write to a key backup version that is not // the user's current one. The spec answers it with the current version, so // that a client whose backup was replaced from another device finds out // which one to use. type WrongBackupVersionError struct { Current string } func (e *WrongBackupVersionError) Error() string { return fmt.Sprintf("backup version is not the current one (%s)", e.Current) } // Rooms says who shares a room with whom, which is who has to be told when // a user's keys change. *roomserver.API satisfies it. type Rooms interface { UsersSharingRooms(ctx context.Context, userID string) ([]string, error) } // Notifier wakes the long polls of users. *syncapi.Notifier satisfies it. type Notifier interface { NotifyUsers(userIDs []string) } // Devices is the part of the userapi the keyserver reads: a user's devices, // for their display names and to tell a device that still exists from one // that does not. *userapi.API satisfies it. type Devices interface { Devices(ctx context.Context, localpart string) ([]storage.Device, error) } // Remote asks other servers about their users' keys. *federation.Client // satisfies it; it refuses every request while federation is off, which is // what keeps a key query from reaching out with the switch down. type Remote interface { QueryUserKeys(ctx context.Context, destination string, req federation.UserKeysQueryRequest) (federation.UserKeysQueryResponse, error) ClaimUserKeys(ctx context.Context, destination string, req federation.UserKeysClaimRequest) (federation.UserKeysClaimResponse, error) UserDevices(ctx context.Context, destination, userID string) (federation.UserDevicesResponse, error) } // Outbound queues EDUs to other servers. *federationapi.Queue satisfies it. type Outbound interface { Running() bool QueueEDU(ctx context.Context, destinations []string, edu federation.EDU) } // API is the keyserver component. type API struct { serverName string store storage.KeyStore rooms Rooms notifier Notifier devices Devices remote Remote outbound Outbound log *slog.Logger // The background work of the remote key cache: fetching stale copies // again, and dropping the copies of users who no longer share a room // with anybody here. background *background resyncs *workQueue sweeps *workQueue } // Options configure the component. type Options struct { // ServerName is the Matrix server name. ServerName string // Store is the key half of storage. Store storage.KeyStore // Rooms says who shares a room with a user. Nil means nobody does, // which is only useful in a test. Rooms Rooms // Notifier wakes the syncs a key change concerns. Nil wakes nobody. Notifier Notifier // Devices is the userapi. Devices Devices // Remote asks other servers for their users' keys. Nil fails every // remote user in a query or claim as unreachable. Remote Remote // Outbound sends device list updates to other servers. Nil, or a // queue that is not running, sends nothing. Outbound Outbound Logger *slog.Logger } // New builds the component. func New(opts Options) (*API, error) { switch { case opts.ServerName == "": return nil, errors.New("keyserver: ServerName must not be empty") case opts.Store == nil: return nil, errors.New("keyserver: Store must not be nil") case opts.Devices == nil: return nil, errors.New("keyserver: Devices must not be nil") } if opts.Logger == nil { opts.Logger = slog.Default() } k := &API{ serverName: opts.ServerName, store: opts.Store, rooms: opts.Rooms, notifier: opts.Notifier, devices: opts.Devices, remote: opts.Remote, outbound: opts.Outbound, log: opts.Logger, background: newBackground(), } k.resyncs = newWorkQueue(k, k.resync) k.sweeps = newWorkQueue(k, k.sweep) return k, nil } // SetOutbound connects the federation queue, which is built after the // keyserver's own dependencies. Nil disconnects it. func (k *API) SetOutbound(o Outbound) { k.outbound = o } // isLocal reports whether a user belongs to this server. func (k *API) isLocal(userID string) bool { server, err := id.ServerNameOf(userID) return err == nil && server == k.serverName } // serverOf returns the server of a user, or "" for a malformed user ID. func serverOf(userID string) string { server, err := id.ServerNameOf(userID) if err != nil { return "" } return server } // localpartOf returns the localpart of a local user. func (k *API) localpartOf(userID string) (string, bool) { localpart, server, err := id.SplitUserID(userID) if err != nil || server != k.serverName { return "", false } return localpart, true } // defaultRemoteTimeout is how long a key query or claim waits for other // servers when the client did not say. A client asking for keys is about // to send a message, and a server that has not answered in ten seconds is // better reported as a failure than waited on. const defaultRemoteTimeout = 10 * time.Second // maxRemoteTimeout caps what a client may ask for. const maxRemoteTimeout = time.Minute // remoteTimeout clamps a client's timeout. func remoteTimeout(d time.Duration) time.Duration { switch { case d <= 0: return defaultRemoteTimeout case d > maxRemoteTimeout: return maxRemoteTimeout } return d } func invalidParamf(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrInvalidParam, fmt.Sprintf(format, args...)) } func missingParamf(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrMissingParam, fmt.Sprintf(format, args...)) } func invalidSignaturef(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrInvalidSignature, fmt.Sprintf(format, args...)) }