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 { mu sync.Mutex sharing map[string][]string } func (f *fakeRooms) UsersSharingRooms(_ context.Context, userID string) ([]string, error) { f.mu.Lock() defer f.mu.Unlock() return slices.Clone(f.sharing[userID]), nil } // set replaces who shares a room with userID. func (f *fakeRooms) set(userID string, users ...string) { f.mu.Lock() defer f.mu.Unlock() f.sharing[userID] = users } 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 rooms *fakeRooms 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.rooms = &fakeRooms{sharing: map[string][]string{ alice: {alice, bob, carol}, bob: {alice, bob}, carol: {alice, carol}, }} h.keys, err = keyserver.New(keyserver.Options{ ServerName: "warp.example", Store: db.Keys(), Rooms: h.rooms, 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) } t.Cleanup(h.keys.Close) 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{ dave: {"V1": json.RawMessage(`{"device_id":"V1"}`)}, "@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 (fakeRemote) UserDevices(context.Context, string, string) (federation.UserDevicesResponse, error) { return federation.UserDevicesResponse{}, errors.New("unreachable") } // dave is a remote user nobody here shares a room with. const dave = "@dave:remote.example" func TestQueryAsksOtherServers(t *testing.T) { h := newHarness(t, fakeRemote{}) resp, err := h.keys.QueryKeys(t.Context(), alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{dave: nil}}) if err != nil { t.Fatal(err) } if _, ok := resp.DeviceKeys[dave]["V1"]; !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) stranger := federation.DeviceListUpdateContent{UserID: dave, DeviceID: "V1", StreamID: 1} if err := h.keys.RemoteDeviceListUpdate(ctx, stranger); err != nil { t.Fatal(err) } if pos, _ := h.store.DeviceListPosition(ctx, dave); pos != 0 { t.Error("an update about a user nobody here shares a room with was recorded") } if _, ok, _ := h.store.RemoteKeys(ctx, dave); ok { t.Error("an update about a stranger left something in the cache") } if err := h.keys.RemoteDeviceListUpdate(ctx, federation.DeviceListUpdateContent{UserID: carol, DeviceID: "C1", StreamID: 1}); 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) } }