Skip to content

Commit

Permalink
Delete server-side backup keys when deactivating an account. (matrix-…
Browse files Browse the repository at this point in the history
  • Loading branch information
H-Shay authored and realtyem committed Apr 12, 2023
1 parent 8c0e4b4 commit dd3f516
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog.d/15181.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Delete server-side backup keys when deactivating an account.
2 changes: 2 additions & 0 deletions synapse/_scripts/synapse_port_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from synapse.storage.databases.main.client_ips import ClientIpBackgroundUpdateStore
from synapse.storage.databases.main.deviceinbox import DeviceInboxBackgroundUpdateStore
from synapse.storage.databases.main.devices import DeviceBackgroundUpdateStore
from synapse.storage.databases.main.e2e_room_keys import EndToEndRoomKeyBackgroundStore
from synapse.storage.databases.main.end_to_end_keys import EndToEndKeyBackgroundStore
from synapse.storage.databases.main.event_push_actions import EventPushActionsStore
from synapse.storage.databases.main.events_bg_updates import (
Expand Down Expand Up @@ -225,6 +226,7 @@ class Store(
MainStateBackgroundUpdateStore,
UserDirectoryBackgroundUpdateStore,
EndToEndKeyBackgroundStore,
EndToEndRoomKeyBackgroundStore,
StatsStore,
AccountDataWorkerStore,
PushRuleStore,
Expand Down
3 changes: 3 additions & 0 deletions synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ async def deactivate_account(
# Remove account data (including ignored users and push rules).
await self.store.purge_account_data_for_user(user_id)

# Delete any server-side backup keys
await self.store.bulk_delete_backup_keys_and_versions_for_user(user_id)

# Let modules know the user has been deactivated.
await self._third_party_rules.on_user_deactivation_status_changed(
user_id,
Expand Down
114 changes: 111 additions & 3 deletions synapse/storage/databases/main/e2e_room_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Iterable, Mapping, Optional, Tuple, cast
from typing import TYPE_CHECKING, Dict, Iterable, Mapping, Optional, Tuple, cast

from typing_extensions import Literal, TypedDict

from synapse.api.errors import StoreError
from synapse.logging.opentracing import log_kv, trace
from synapse.storage._base import SQLBaseStore, db_to_json
from synapse.storage.database import LoggingTransaction
from synapse.storage.database import (
DatabasePool,
LoggingDatabaseConnection,
LoggingTransaction,
)
from synapse.types import JsonDict, JsonSerializable, StreamKeyType
from synapse.util import json_encoder

if TYPE_CHECKING:
from synapse.server import HomeServer


class RoomKey(TypedDict):
"""`KeyBackupData` in the Matrix spec.
Expand All @@ -37,7 +44,82 @@ class RoomKey(TypedDict):
session_data: JsonSerializable


class EndToEndRoomKeyStore(SQLBaseStore):
class EndToEndRoomKeyBackgroundStore(SQLBaseStore):
def __init__(
self,
database: DatabasePool,
db_conn: LoggingDatabaseConnection,
hs: "HomeServer",
):
super().__init__(database, db_conn, hs)

self.db_pool.updates.register_background_update_handler(
"delete_e2e_backup_keys_for_deactivated_users",
self._delete_e2e_backup_keys_for_deactivated_users,
)

def _delete_keys_txn(self, txn: LoggingTransaction, user_id: str) -> None:
self.db_pool.simple_delete_txn(
txn,
table="e2e_room_keys",
keyvalues={"user_id": user_id},
)

self.db_pool.simple_delete_txn(
txn,
table="e2e_room_keys_versions",
keyvalues={"user_id": user_id},
)

async def _delete_e2e_backup_keys_for_deactivated_users(
self, progress: JsonDict, batch_size: int
) -> int:
"""
Retroactively purges account data for users that have already been deactivated.
Gets run as a background update caused by a schema delta.
"""

last_user: str = progress.get("last_user", "")

def _delete_backup_keys_for_deactivated_users_txn(
txn: LoggingTransaction,
) -> int:
sql = """
SELECT name FROM users
WHERE deactivated = ? and name > ?
ORDER BY name ASC
LIMIT ?
"""

txn.execute(sql, (1, last_user, batch_size))
users = [row[0] for row in txn]

for user in users:
self._delete_keys_txn(txn, user)

if users:
self.db_pool.updates._background_update_progress_txn(
txn,
"delete_e2e_backup_keys_for_deactivated_users",
{"last_user": users[-1]},
)

return len(users)

number_deleted = await self.db_pool.runInteraction(
"_delete_backup_keys_for_deactivated_users",
_delete_backup_keys_for_deactivated_users_txn,
)

if number_deleted < batch_size:
await self.db_pool.updates._end_background_update(
"delete_e2e_backup_keys_for_deactivated_users"
)

return number_deleted


class EndToEndRoomKeyStore(EndToEndRoomKeyBackgroundStore):
"""The store for end to end room key backups.
See https://spec.matrix.org/v1.1/client-server-api/#server-side-key-backups
Expand Down Expand Up @@ -550,3 +632,29 @@ def _delete_e2e_room_keys_version_txn(txn: LoggingTransaction) -> None:
await self.db_pool.runInteraction(
"delete_e2e_room_keys_version", _delete_e2e_room_keys_version_txn
)

async def bulk_delete_backup_keys_and_versions_for_user(self, user_id: str) -> None:
"""
Bulk deletes all backup room keys and versions for a given user.
Args:
user_id: the user whose backup keys and versions we're deleting
"""

def _delete_all_e2e_room_keys_and_versions_txn(txn: LoggingTransaction) -> None:
self.db_pool.simple_delete_txn(
txn,
table="e2e_room_keys",
keyvalues={"user_id": user_id},
)

self.db_pool.simple_delete_txn(
txn,
table="e2e_room_keys_versions",
keyvalues={"user_id": user_id},
)

await self.db_pool.runInteraction(
"delete_all_e2e_room_keys_and_versions",
_delete_all_e2e_room_keys_and_versions_txn,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
(7404, 'delete_e2e_backup_keys_for_deactivated_users', '{}');
157 changes: 157 additions & 0 deletions tests/rest/client/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,163 @@ def test_pending_invites(self) -> None:
self.assertEqual(len(memberships), 1, memberships)
self.assertEqual(memberships[0].room_id, room_id, memberships)

def test_deactivate_account_deletes_server_side_backup_keys(self) -> None:
key_handler = self.hs.get_e2e_room_keys_handler()
room_keys = {
"rooms": {
"!abc:matrix.org": {
"sessions": {
"c0ff33": {
"first_message_index": 1,
"forwarded_count": 1,
"is_verified": False,
"session_data": "SSBBTSBBIEZJU0gK",
}
}
}
}
}

user_id = self.register_user("missPiggy", "test")
tok = self.login("missPiggy", "test")

# add some backup keys/versions
version = self.get_success(
key_handler.create_version(
user_id,
{
"algorithm": "m.megolm_backup.v1",
"auth_data": "first_version_auth_data",
},
)
)

self.get_success(key_handler.upload_room_keys(user_id, version, room_keys))

version2 = self.get_success(
key_handler.create_version(
user_id,
{
"algorithm": "m.megolm_backup.v1",
"auth_data": "second_version_auth_data",
},
)
)

self.get_success(key_handler.upload_room_keys(user_id, version2, room_keys))

self.deactivate(user_id, tok)
store = self.hs.get_datastores().main

# Check that the user has been marked as deactivated.
self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id)))

# Check that there are no entries in 'e2e_room_keys` and `e2e_room_keys_versions`
res = self.get_success(
self.hs.get_datastores().main.db_pool.simple_select_list(
"e2e_room_keys", {"user_id": user_id}, "*", "simple_select"
)
)
self.assertEqual(len(res), 0)

res2 = self.get_success(
self.hs.get_datastores().main.db_pool.simple_select_list(
"e2e_room_keys_versions", {"user_id": user_id}, "*", "simple_select"
)
)
self.assertEqual(len(res2), 0)

def test_background_update_deletes_deactivated_users_server_side_backup_keys(
self,
) -> None:
key_handler = self.hs.get_e2e_room_keys_handler()
room_keys = {
"rooms": {
"!abc:matrix.org": {
"sessions": {
"c0ff33": {
"first_message_index": 1,
"forwarded_count": 1,
"is_verified": False,
"session_data": "SSBBTSBBIEZJU0gK",
}
}
}
}
}
self.store = self.hs.get_datastores().main

# create a bunch of users and add keys for them
users = []
for i in range(0, 20):
user_id = self.register_user("missPiggy" + str(i), "test")
users.append((user_id,))

# add some backup keys/versions
version = self.get_success(
key_handler.create_version(
user_id,
{
"algorithm": "m.megolm_backup.v1",
"auth_data": str(i) + "_version_auth_data",
},
)
)

self.get_success(key_handler.upload_room_keys(user_id, version, room_keys))

version2 = self.get_success(
key_handler.create_version(
user_id,
{
"algorithm": "m.megolm_backup.v1",
"auth_data": str(i) + "_version_auth_data",
},
)
)

self.get_success(key_handler.upload_room_keys(user_id, version2, room_keys))

# deactivate most of the users by editing DB
self.get_success(
self.store.db_pool.simple_update_many(
table="users",
key_names=("name",),
key_values=users[0:18],
value_names=("deactivated",),
value_values=[(1,) for i in range(1, 19)],
desc="",
)
)

# run background update
self.get_success(
self.store.db_pool.simple_insert(
"background_updates",
{
"update_name": "delete_e2e_backup_keys_for_deactivated_users",
"progress_json": "{}",
},
)
)
self.store.db_pool.updates._all_done = False
self.wait_for_background_updates()

# check that keys are deleted for the deactivated users but not the others
res = self.get_success(
self.hs.get_datastores().main.db_pool.simple_select_list(
"e2e_room_keys", None, ("user_id",), "simple_select"
)
)
self.assertEqual(len(res), 4)

res2 = self.get_success(
self.hs.get_datastores().main.db_pool.simple_select_list(
"e2e_room_keys_versions", None, ("user_id",), "simple_select"
)
)
self.assertEqual(len(res2), 4)

def deactivate(self, user_id: str, tok: str) -> None:
request_data = {
"auth": {
Expand Down

0 comments on commit dd3f516

Please sign in to comment.