Files
Warp/server/internal/keyserver/keyserver_test.go
T
tajniak81andClaude Opus 5.5 fc49f56339 Phase 7: E2EE support
Implements PLAN.md Phase 7: the server half of end-to-end encryption.
Devices upload identity, one-time and fallback keys; other devices query
and claim them, across servers too, and every one-time key is handed out
exactly once. Cross-signing, signature uploads, device list tracking and
server-side key backup complete the set.

internal/keyserver is the component. Keys are stored in canonical JSON
so that re-uploading the same key is a byte comparison, and signatures
uploaded later are kept apart from the key they sign and merged in on
read, so a key's own JSON stays what its owner uploaded. Device keys must
be for the caller's own device; cross-signing keys must be the
uploader's, carry the right usage and, for self- and user-signing keys,
be signed by the master key. Signatures are verified against the key as
stored. Replacing a master key asks for the password; a first upload or
an upload of the keys already stored does not, as the spec allows. A
signature of another user's master key is shown only to the user who
made it and never federated.

Every write that changes what /keys/query answers moves the device list
stream in the same transaction, one position per device, and wakes the
syncs of everyone sharing a room. The stream is the sixth field of a sync
token; older tokens still parse. The syncapi works out device_lists
changed and left from the stream and from membership moves in the
window, skipping invites and knocks, and reports
device_one_time_keys_count and device_unused_fallback_key_types, the
latter as [] when empty. GET /keys/changes uses the same logic.

Over federation: /user/keys/query, /user/keys/claim and
/user/devices/{userId}, and m.device_list_update and m.signing_key_update
both ways. Remote keys are not cached; an update about a remote user
sharing a room moves them in the stream so local clients re-query.
Device display names are not sent to other servers. With federation off,
remote users are reported in failures at once.

Deleting a device deletes its keys, through a new userapi
DeviceObserver. Key backup keeps the better copy of each session by the
spec's rule, never reuses a deleted version's number, and answers writes
to a stale version with M_WRONG_ROOM_KEYS_VERSION.

Storage is migration 0008 for sqlite and postgres, with a PocketBase
stub. The Complement target sets gain the key, device list, fallback,
cross-signing and backup tests.

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

624 lines
22 KiB
Go

package keyserver_test
import (
"context"
"encoding/json"
"errors"
"path/filepath"
"slices"
"sync"
"testing"
"github.com/dariuszjaniak/warp/server/internal/federation"
"github.com/dariuszjaniak/warp/server/internal/keyserver"
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
"github.com/dariuszjaniak/warp/server/internal/storage"
_ "github.com/dariuszjaniak/warp/server/internal/storage/drivers"
)
const (
alice = "@alice:warp.example"
bob = "@bob:warp.example"
carol = "@carol:remote.example"
)
// --- fakes --------------------------------------------------------------------
type fakeRooms struct{ sharing map[string][]string }
func (f fakeRooms) UsersSharingRooms(_ context.Context, userID string) ([]string, error) {
return f.sharing[userID], nil
}
type fakeNotifier struct {
mu sync.Mutex
woken []string
}
func (n *fakeNotifier) NotifyUsers(userIDs []string) {
n.mu.Lock()
defer n.mu.Unlock()
n.woken = append(n.woken, userIDs...)
}
func (n *fakeNotifier) wasWoken(userID string) bool {
n.mu.Lock()
defer n.mu.Unlock()
return slices.Contains(n.woken, userID)
}
type fakeDevices struct{ byLocalpart map[string][]storage.Device }
func (f fakeDevices) Devices(_ context.Context, localpart string) ([]storage.Device, error) {
return f.byLocalpart[localpart], nil
}
type sentEDU struct {
destinations []string
edu federation.EDU
}
type fakeOutbound struct {
mu sync.Mutex
sent []sentEDU
}
func (o *fakeOutbound) Running() bool { return true }
func (o *fakeOutbound) QueueEDU(_ context.Context, destinations []string, edu federation.EDU) {
o.mu.Lock()
defer o.mu.Unlock()
o.sent = append(o.sent, sentEDU{destinations: destinations, edu: edu})
}
func (o *fakeOutbound) ofType(t string) []sentEDU {
o.mu.Lock()
defer o.mu.Unlock()
var out []sentEDU
for _, s := range o.sent {
if s.edu.Type == t {
out = append(out, s)
}
}
return out
}
type harness struct {
keys *keyserver.API
store storage.KeyStore
notifier *fakeNotifier
outbound *fakeOutbound
}
func newHarness(t *testing.T, remote keyserver.Remote) harness {
t.Helper()
path := filepath.ToSlash(filepath.Join(t.TempDir(), "warp.db"))
db, err := storage.Open(t.Context(), storage.Config{
Driver: storage.DriverSQLite,
ConnectionString: "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)",
}, nil)
if err != nil {
t.Fatalf("storage.Open: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if err := db.Migrate(t.Context()); err != nil {
t.Fatalf("Migrate: %v", err)
}
h := harness{store: db.Keys(), notifier: &fakeNotifier{}, outbound: &fakeOutbound{}}
h.keys, err = keyserver.New(keyserver.Options{
ServerName: "warp.example",
Store: db.Keys(),
Rooms: fakeRooms{sharing: map[string][]string{
alice: {alice, bob, carol},
bob: {alice, bob},
carol: {alice, carol},
}},
Notifier: h.notifier,
Devices: fakeDevices{byLocalpart: map[string][]storage.Device{
"alice": {{Localpart: "alice", ID: "D1", DisplayName: "Alice's phone"}, {Localpart: "alice", ID: "D2"}},
"bob": {{Localpart: "bob", ID: "B1"}},
}},
Remote: remote,
Outbound: h.outbound,
})
if err != nil {
t.Fatalf("keyserver.New: %v", err)
}
return h
}
// --- keys ---------------------------------------------------------------------
// newKey makes an ed25519 key named by its own public key, which is how a
// cross-signing key is named; a device key is renamed to its device ID.
func newKey(t *testing.T) *signing.KeyPair {
t.Helper()
kp, err := signing.Generate("")
if err != nil {
t.Fatal(err)
}
kp.ID = kp.PublicBase64()
return kp
}
func signed(t *testing.T, userID string, kp *signing.KeyPair, v any) json.RawMessage {
t.Helper()
raw, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
out, err := signing.SignJSON(userID, kp, raw)
if err != nil {
t.Fatal(err)
}
return out
}
// device is a device's identity key pair and its signed device_keys.
func device(t *testing.T, userID, deviceID string) (*signing.KeyPair, json.RawMessage) {
t.Helper()
kp := newKey(t)
pub := kp.PublicBase64()
kp.ID = deviceID
return kp, signed(t, userID, kp, map[string]any{
"user_id": userID,
"device_id": deviceID,
"algorithms": []string{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
"keys": map[string]string{
"ed25519:" + deviceID: pub,
"curve25519:" + deviceID: "curve-" + deviceID,
},
})
}
func crossSigningKey(userID, usage string, kp *signing.KeyPair) map[string]any {
return map[string]any{
"user_id": userID,
"usage": []string{usage},
"keys": map[string]string{"ed25519:" + kp.PublicBase64(): kp.PublicBase64()},
}
}
// crossSigning is a user's three cross-signing keys, uploaded.
type crossSigning struct {
master, selfSigning, userSigning *signing.KeyPair
req keyserver.CrossSigningRequest
}
func newCrossSigning(t *testing.T, userID string) crossSigning {
t.Helper()
cs := crossSigning{master: newKey(t), selfSigning: newKey(t), userSigning: newKey(t)}
cs.req = keyserver.CrossSigningRequest{
MasterKey: signed(t, userID, newKey(t), crossSigningKey(userID, "master", cs.master)),
SelfSigningKey: signed(t, userID, cs.master, crossSigningKey(userID, "self_signing", cs.selfSigning)),
UserSigningKey: signed(t, userID, cs.master, crossSigningKey(userID, "user_signing", cs.userSigning)),
}
// The master key's own signature above is by a throwaway key, which
// the server does not check; what it checks is that the master key
// signed the other two.
raw, _ := json.Marshal(crossSigningKey(userID, "master", cs.master))
cs.req.MasterKey = raw
return cs
}
func decodeObject(t *testing.T, raw json.RawMessage) map[string]any {
t.Helper()
var out map[string]any
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatalf("decode %s: %v", raw, err)
}
return out
}
func signaturesBy(t *testing.T, raw json.RawMessage, userID string) map[string]any {
t.Helper()
sigs, _ := decodeObject(t, raw)["signatures"].(map[string]any)
out, _ := sigs[userID].(map[string]any)
return out
}
// --- tests --------------------------------------------------------------------
func TestUploadAndQueryDeviceKeys(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
_, d1 := device(t, alice, "D1")
counts, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
DeviceKeys: d1,
OneTimeKeys: map[string]json.RawMessage{
"signed_curve25519:AAA": json.RawMessage(`{"key":"a"}`),
"signed_curve25519:BBB": json.RawMessage(`{"key":"b"}`),
},
FallbackKeys: map[string]json.RawMessage{
"signed_curve25519:FFF": json.RawMessage(`{"key":"f","fallback":true}`),
},
})
if err != nil {
t.Fatalf("UploadKeys: %v", err)
}
if counts["signed_curve25519"] != 2 {
t.Errorf("one_time_key_counts = %v, want 2 signed_curve25519", counts)
}
// Everybody sharing a room hears of it: the local ones are woken, the
// remote server gets an m.device_list_update with the keys in it.
if !h.notifier.wasWoken(bob) {
t.Error("a user sharing a room was not woken")
}
updates := h.outbound.ofType(federation.EDUDeviceListUp)
if len(updates) != 1 || !slices.Equal(updates[0].destinations, []string{"remote.example"}) {
t.Fatalf("device list EDUs = %+v, want one to remote.example", updates)
}
var content federation.DeviceListUpdateContent
_ = json.Unmarshal(updates[0].edu.Content, &content)
if content.UserID != alice || content.DeviceID != "D1" || content.Deleted || len(content.Keys) == 0 || content.StreamID == 0 {
t.Errorf("EDU content = %+v", content)
}
resp, err := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
if err != nil {
t.Fatalf("QueryKeys: %v", err)
}
got, ok := resp.DeviceKeys[alice]["D1"]
if !ok {
t.Fatalf("query did not return D1: %+v", resp.DeviceKeys)
}
unsigned, _ := decodeObject(t, got)["unsigned"].(map[string]any)
if unsigned["device_display_name"] != "Alice's phone" {
t.Errorf("unsigned = %v, want the device's display name", unsigned)
}
if _, err := signing.SigningBytes(got); err != nil {
t.Errorf("queried keys are not a signed object: %v", err)
}
// Somebody else's device, or keys naming another device, are refused.
_, other := device(t, alice, "D2")
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: other}); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("keys for another device = %v, want ErrInvalidParam", err)
}
_, bobs := device(t, bob, "D1")
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: bobs}); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("keys for another user = %v, want ErrInvalidParam", err)
}
// The same one-time key again is fine, encoded differently or not; a
// different key under its ID is not.
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{ "key" : "a" }`)},
}); err != nil {
t.Errorf("re-uploading the same key: %v", err)
}
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{"key":"z"}`)},
}); !errors.Is(err, keyserver.ErrKeyConflict) {
t.Errorf("a conflicting key = %v, want ErrKeyConflict", err)
}
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
OneTimeKeys: map[string]json.RawMessage{"no-algorithm": json.RawMessage(`"x"`)},
}); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("a key ID with no algorithm = %v, want ErrInvalidParam", err)
}
}
func TestClaimKeys(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
_, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{"key":"a"}`)},
FallbackKeys: map[string]json.RawMessage{"signed_curve25519:FFF": json.RawMessage(`{"key":"f"}`)},
})
if err != nil {
t.Fatal(err)
}
claim := func() keyserver.ClaimResponse {
t.Helper()
resp, err := h.keys.ClaimKeys(ctx, keyserver.ClaimRequest{OneTimeKeys: map[string]map[string]string{
alice: {"D1": "signed_curve25519", "D2": "signed_curve25519"},
carol: {"C1": "signed_curve25519"},
}})
if err != nil {
t.Fatalf("ClaimKeys: %v", err)
}
return resp
}
first := claim()
if _, ok := first.OneTimeKeys[alice]["D1"]["signed_curve25519:AAA"]; !ok {
t.Errorf("first claim = %v, want the one-time key", first.OneTimeKeys)
}
if _, ok := first.OneTimeKeys[alice]["D2"]; ok {
t.Error("a device with no keys was given one")
}
if _, ok := first.Failures["remote.example"]; !ok {
t.Errorf("a server that cannot be asked is not a failure: %v", first.Failures)
}
second := claim()
if _, ok := second.OneTimeKeys[alice]["D1"]["signed_curve25519:FFF"]; !ok {
t.Errorf("second claim = %v, want the fallback key", second.OneTimeKeys)
}
if types, _ := h.keys.UnusedFallbackKeyTypes(ctx, alice, "D1"); len(types) != 0 {
t.Errorf("a claimed fallback key is still unused: %v", types)
}
}
// fakeRemote answers for remote.example.
type fakeRemote struct{}
func (fakeRemote) QueryUserKeys(_ context.Context, _ string, req federation.UserKeysQueryRequest) (federation.UserKeysQueryResponse, error) {
return federation.UserKeysQueryResponse{
DeviceKeys: map[string]map[string]json.RawMessage{
carol: {"C1": json.RawMessage(`{"device_id":"C1"}`)},
"@mallory:other.test": {"M1": json.RawMessage(`{}`)},
},
}, nil
}
func (fakeRemote) ClaimUserKeys(context.Context, string, federation.UserKeysClaimRequest) (federation.UserKeysClaimResponse, error) {
return federation.UserKeysClaimResponse{}, errors.New("unreachable")
}
func TestQueryAsksOtherServers(t *testing.T) {
h := newHarness(t, fakeRemote{})
resp, err := h.keys.QueryKeys(t.Context(), alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{carol: nil}})
if err != nil {
t.Fatal(err)
}
if _, ok := resp.DeviceKeys[carol]["C1"]; !ok {
t.Errorf("remote keys = %v", resp.DeviceKeys)
}
if _, ok := resp.DeviceKeys["@mallory:other.test"]; ok {
t.Error("a server's answer about a user it was not asked about was passed on")
}
claimed, _ := h.keys.ClaimKeys(t.Context(), keyserver.ClaimRequest{OneTimeKeys: map[string]map[string]string{carol: {"C1": "signed_curve25519"}}})
if failure, ok := claimed.Failures["remote.example"].(map[string]any); !ok || failure["status"] != 503 {
t.Errorf("failures = %v, want a 503 for remote.example", claimed.Failures)
}
}
func TestCrossSigning(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
cs := newCrossSigning(t, alice)
if needs, err := h.keys.CrossSigningNeedsAuth(ctx, alice, cs.req); err != nil || needs {
t.Fatalf("a first upload needs auth = %v, %v; want false", needs, err)
}
if err := h.keys.UploadCrossSigningKeys(ctx, alice, cs.req); err != nil {
t.Fatalf("UploadCrossSigningKeys: %v", err)
}
if updates := h.outbound.ofType(federation.EDUSigningKeys); len(updates) != 1 {
t.Errorf("signing key EDUs = %d, want 1", len(updates))
}
// The same keys again need no password; a new master key does.
if needs, _ := h.keys.CrossSigningNeedsAuth(ctx, alice, cs.req); needs {
t.Error("re-uploading the same keys needs auth")
}
replacement := newCrossSigning(t, alice)
if needs, _ := h.keys.CrossSigningNeedsAuth(ctx, alice, replacement.req); !needs {
t.Error("replacing the master key needs no auth")
}
// The user-signing key is only shown to its owner.
mine, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
theirs, _ := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
if _, ok := mine.UserSigningKeys[alice]; !ok {
t.Error("the owner does not see their user-signing key")
}
if _, ok := theirs.UserSigningKeys[alice]; ok {
t.Error("another user sees the user-signing key")
}
if _, ok := theirs.MasterKeys[alice]; !ok {
t.Error("another user does not see the master key")
}
if _, ok := theirs.SelfSigningKeys[alice]; !ok {
t.Error("another user does not see the self-signing key")
}
// A self-signing key the master key did not sign is refused.
forged := keyserver.CrossSigningRequest{
SelfSigningKey: signed(t, alice, newKey(t), crossSigningKey(alice, "self_signing", newKey(t))),
}
if err := h.keys.UploadCrossSigningKeys(ctx, alice, forged); !errors.Is(err, keyserver.ErrInvalidSignature) {
t.Errorf("a self-signing key not signed by the master = %v, want ErrInvalidSignature", err)
}
wrongUsage := keyserver.CrossSigningRequest{
SelfSigningKey: signed(t, alice, cs.master, crossSigningKey(alice, "user_signing", newKey(t))),
}
if err := h.keys.UploadCrossSigningKeys(ctx, alice, wrongUsage); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("a key with the wrong usage = %v, want ErrInvalidParam", err)
}
// A new master key on its own drops the keys the old one signed.
if err := h.keys.UploadCrossSigningKeys(ctx, alice, keyserver.CrossSigningRequest{MasterKey: replacement.req.MasterKey}); err != nil {
t.Fatalf("replacing the master key: %v", err)
}
after, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
if _, ok := after.SelfSigningKeys[alice]; ok {
t.Error("the old self-signing key survived a new master key")
}
// And nothing but a master key needs one first.
if err := h.keys.UploadCrossSigningKeys(ctx, bob, keyserver.CrossSigningRequest{
SelfSigningKey: signed(t, bob, newKey(t), crossSigningKey(bob, "self_signing", newKey(t))),
}); !errors.Is(err, keyserver.ErrMissingParam) {
t.Errorf("a self-signing key with no master = %v, want ErrMissingParam", err)
}
}
func TestSignatureUploads(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
deviceKey, d1 := device(t, alice, "D1")
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: d1}); err != nil {
t.Fatal(err)
}
cs := newCrossSigning(t, alice)
if err := h.keys.UploadCrossSigningKeys(ctx, alice, cs.req); err != nil {
t.Fatal(err)
}
bobs := newCrossSigning(t, bob)
if err := h.keys.UploadCrossSigningKeys(ctx, bob, bobs.req); err != nil {
t.Fatal(err)
}
var stripped map[string]any
_ = json.Unmarshal(d1, &stripped)
delete(stripped, "signatures")
masterPub := cs.master.PublicBase64()
failures, err := h.keys.UploadSignatures(ctx, alice, map[string]map[string]json.RawMessage{
alice: {
// The device, by the self-signing key.
"D1": signed(t, alice, cs.selfSigning, stripped),
// The master key, by the device.
masterPub: signed(t, alice, deviceKey, crossSigningKey(alice, "master", cs.master)),
},
// Bob's master key, by Alice's user-signing key.
bob: {bobs.master.PublicBase64(): signed(t, alice, cs.userSigning, crossSigningKey(bob, "master", bobs.master))},
})
if err != nil || len(failures) != 0 {
t.Fatalf("UploadSignatures = %v, %v", failures, err)
}
resp, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil, bob: nil}})
if _, ok := signaturesBy(t, resp.DeviceKeys[alice]["D1"], alice)["ed25519:"+cs.selfSigning.PublicBase64()]; !ok {
t.Error("the device is not shown signed by the self-signing key")
}
if _, ok := signaturesBy(t, resp.MasterKeys[alice], alice)["ed25519:D1"]; !ok {
t.Error("the master key is not shown signed by the device")
}
if _, ok := signaturesBy(t, resp.MasterKeys[bob], alice)["ed25519:"+cs.userSigning.PublicBase64()]; !ok {
t.Error("Alice does not see her signature of Bob's master key")
}
// Who Alice has verified is between her and her devices.
asCarol, _ := h.keys.QueryKeys(ctx, carol, keyserver.QueryRequest{DeviceKeys: map[string][]string{bob: nil}})
if sigs := signaturesBy(t, asCarol.MasterKeys[bob], alice); len(sigs) != 0 {
t.Errorf("another user sees Alice's signature of Bob's key: %v", sigs)
}
// A signature that does not verify against the stored key is refused,
// and so is one of a key that does not exist.
stripped["algorithms"] = []string{"tampered"}
failures, err = h.keys.UploadSignatures(ctx, alice, map[string]map[string]json.RawMessage{
alice: {
"D1": signed(t, alice, cs.selfSigning, stripped),
"NOPE": signed(t, alice, cs.selfSigning, stripped),
},
})
if err != nil {
t.Fatal(err)
}
if failures[alice]["D1"].ErrCode != "M_INVALID_SIGNATURE" {
t.Errorf("a signature over a different object = %+v", failures[alice]["D1"])
}
if failures[alice]["NOPE"].ErrCode != "M_NOT_FOUND" {
t.Errorf("a signature of an unknown device = %+v", failures[alice]["NOPE"])
}
}
func TestDeletedDevicesLoseTheirKeys(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
_, d1 := device(t, alice, "D1")
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
DeviceKeys: d1,
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{}`)},
}); err != nil {
t.Fatal(err)
}
h.keys.DevicesDeleted(ctx, "alice", []string{"D1"})
resp, _ := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
if len(resp.DeviceKeys[alice]) != 0 {
t.Errorf("keys of a deleted device are still served: %v", resp.DeviceKeys[alice])
}
if counts, _ := h.keys.OneTimeKeyCounts(ctx, alice, "D1"); counts["signed_curve25519"] != 0 {
t.Errorf("one-time keys of a deleted device survived: %v", counts)
}
updates := h.outbound.ofType(federation.EDUDeviceListUp)
var last federation.DeviceListUpdateContent
_ = json.Unmarshal(updates[len(updates)-1].edu.Content, &last)
if !last.Deleted || last.DeviceID != "D1" || len(last.PrevID) != 1 {
t.Errorf("the last device list EDU = %+v, want D1 deleted after its upload", last)
}
}
func TestRemoteKeyChangesOnlyConcernSharedUsers(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
if err := h.keys.RemoteKeysChanged(ctx, "@stranger:remote.example"); err != nil {
t.Fatal(err)
}
if pos, _ := h.store.DeviceListPosition(ctx, "@stranger:remote.example"); pos != 0 {
t.Error("an update about a user nobody here shares a room with was recorded")
}
if err := h.keys.RemoteKeysChanged(ctx, carol); err != nil {
t.Fatal(err)
}
if pos, _ := h.store.DeviceListPosition(ctx, carol); pos == 0 {
t.Error("an update about a user sharing a room was not recorded")
}
if !h.notifier.wasWoken(alice) {
t.Error("the local user sharing a room was not woken")
}
if len(h.outbound.sent) != 0 {
t.Errorf("a remote update was echoed over federation: %+v", h.outbound.sent)
}
}
func TestKeyBackupVersions(t *testing.T) {
ctx := t.Context()
h := newHarness(t, nil)
v1, err := h.keys.CreateBackup(ctx, alice, "m.megolm_backup.v1.curve25519-aes-sha2", json.RawMessage(`{"public_key":"a"}`))
if err != nil {
t.Fatal(err)
}
if _, err := h.keys.CreateBackup(ctx, alice, "", json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrMissingParam) {
t.Errorf("a backup with no algorithm = %v", err)
}
if _, err := h.keys.CreateBackup(ctx, alice, "alg", json.RawMessage(`[1]`)); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("auth_data that is not an object = %v", err)
}
rooms := map[string]keyserver.RoomKeyBackup{"!r:x": {Sessions: map[string]keyserver.KeyBackupData{
"s1": {FirstMessageIndex: 1, SessionData: json.RawMessage(`{"ciphertext":"c"}`)},
}}}
result, err := h.keys.PutBackupKeys(ctx, alice, v1, rooms)
if err != nil || result.Count != 1 {
t.Fatalf("PutBackupKeys = %+v, %v", result, err)
}
v2, _ := h.keys.CreateBackup(ctx, alice, "m.megolm_backup.v1.curve25519-aes-sha2", json.RawMessage(`{"public_key":"b"}`))
var wrong *keyserver.WrongBackupVersionError
if _, err := h.keys.PutBackupKeys(ctx, alice, v1, rooms); !errors.As(err, &wrong) || wrong.Current != v2 {
t.Errorf("writing to an old version = %v, want WrongBackupVersionError{%s}", err, v2)
}
if _, err := h.keys.PutBackupKeys(ctx, alice, "99", rooms); !errors.Is(err, keyserver.ErrNoSuchBackup) {
t.Errorf("writing to a version that does not exist = %v", err)
}
if err := h.keys.UpdateBackup(ctx, alice, v2, "another.algorithm", "", json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("changing a version's algorithm = %v", err)
}
if err := h.keys.UpdateBackup(ctx, alice, v2, "m.megolm_backup.v1.curve25519-aes-sha2", v1, json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrInvalidParam) {
t.Errorf("a body version that is not the path's = %v", err)
}
old, err := h.keys.BackupKeys(ctx, alice, v1, "", "")
if err != nil || len(old["!r:x"].Sessions) != 1 {
t.Errorf("an old version's keys = %v, %v; still readable", old, err)
}
}