From 4fb8481492c5a9992830f0168dd8f6e86c5d6ca5 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 16 Dec 2024 10:14:58 +0100 Subject: [PATCH] feat: Add option to disable end-to-end encryption to allow legacy clients. Signed-off-by: Joachim Bauch --- appinfo/routes/routesRoomController.php | 2 + lib/Controller/RoomController.php | 29 +++++++ lib/Controller/SignalingController.php | 1 + lib/Events/ARoomModifiedEvent.php | 9 +- lib/Manager.php | 2 + .../Version21000Date20241212134329.php | 39 +++++++++ lib/Model/SelectHelper.php | 1 + lib/Room.php | 10 +++ lib/Service/RoomFormatter.php | 3 + lib/Service/RoomService.php | 22 +++++ lib/Signaling/Listener.php | 1 + .../ConversationSettingsDialog.vue | 3 + .../ConversationSettings/SecuritySettings.vue | 84 +++++++++++++++++++ src/services/conversationsService.js | 13 +++ src/store/conversationsStore.js | 15 ++++ src/utils/signaling.js | 4 + src/utils/webrtc/index.js | 17 ++++ 17 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 lib/Migration/Version21000Date20241212134329.php create mode 100644 src/components/ConversationSettings/SecuritySettings.vue diff --git a/appinfo/routes/routesRoomController.php b/appinfo/routes/routesRoomController.php index 9bff6349ecf..9af50f4b155 100644 --- a/appinfo/routes/routesRoomController.php +++ b/appinfo/routes/routesRoomController.php @@ -103,6 +103,8 @@ ['name' => 'Room#setLobby', 'url' => '/api/{apiVersion}/room/{token}/webinar/lobby', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::setSIPEnabled() */ ['name' => 'Room#setSIPEnabled', 'url' => '/api/{apiVersion}/room/{token}/webinar/sip', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], + /** @see \OCA\Talk\Controller\RoomController::setEncryptionEnabled() */ + ['name' => 'Room#setEncryptionEnabled', 'url' => '/api/{apiVersion}/room/{token}/webinar/encryption', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::setRecordingConsent() */ ['name' => 'Room#setRecordingConsent', 'url' => '/api/{apiVersion}/room/{token}/recording-consent', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::setMessageExpiration() */ diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 70b353c7b06..6347c15a708 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -2369,6 +2369,35 @@ public function setSIPEnabled(int $state): DataResponse { return new DataResponse($this->formatRoom($this->room, $this->participant)); } + /** + * Update end-to-end encryption enabled state + * + * @param bool $state New state + * @psalm-param Webinary::SIP_* $state + * @return DataResponse|DataResponse|DataResponse + * + * 200: End-to-end encryption enabled state updated successfully + * 400: Updating end-to-end encryption enabled state is not possible + * 401: User not found + * 403: Missing permissions to update end-to-end encryption enabled state + */ + #[NoAdminRequired] + #[RequireModeratorParticipant] + public function setEncryptionEnabled(bool $enabled): DataResponse { + $user = $this->userManager->get($this->userId); + if (!$user instanceof IUser) { + return new DataResponse(['error' => 'config'], Http::STATUS_UNAUTHORIZED); + } + + try { + $this->roomService->setEncryptionEnabled($this->room, $enabled); + } catch (SipConfigurationException $e) { + return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } + /** * Set recording consent requirement for this conversation * diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 2731d42ff77..57df1422ba3 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -218,6 +218,7 @@ public function getSettings(string $token = ''): DataResponse { 'stunservers' => $stun, 'turnservers' => $turn, 'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '', + 'encrypted' => $room->getEncryptionEnabled(), ]; return new DataResponse($data); diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index 72a8f415df1..d1c84a0f1c9 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -31,6 +31,7 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public const PROPERTY_READ_ONLY = 'readOnly'; public const PROPERTY_RECORDING_CONSENT = 'recordingConsent'; public const PROPERTY_SIP_ENABLED = 'sipEnabled'; + public const PROPERTY_ENCRYPTION_ENABLED = 'encryptionEnabled'; public const PROPERTY_TYPE = 'type'; /** @@ -39,8 +40,8 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public function __construct( Room $room, protected string $property, - protected \DateTime|string|int|null $newValue, - protected \DateTime|string|int|null $oldValue = null, + protected \DateTime|string|int|bool|null $newValue, + protected \DateTime|string|int|bool|null $oldValue = null, protected ?Participant $actor = null, ) { parent::__construct($room); @@ -50,11 +51,11 @@ public function getProperty(): string { return $this->property; } - public function getNewValue(): \DateTime|string|int|null { + public function getNewValue(): \DateTime|string|int|bool|null { return $this->newValue; } - public function getOldValue(): \DateTime|string|int|null { + public function getOldValue(): \DateTime|string|int|bool|null { return $this->oldValue; } diff --git a/lib/Manager.php b/lib/Manager.php index a4e87a89efb..4a1e1b4eb4f 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -98,6 +98,7 @@ public function createRoomObjectFromData(array $data): Room { 'message_expiration' => 0, 'lobby_state' => 0, 'sip_enabled' => 0, + 'encrypted' => false, 'assigned_hpb' => null, 'token' => '', 'name' => '', @@ -167,6 +168,7 @@ public function createRoomObject(array $row): Room { (int)$row['message_expiration'], (int)$row['lobby_state'], (int)$row['sip_enabled'], + (bool)$row['encrypted'], $assignedSignalingServer, (string)$row['token'], (string)$row['name'], diff --git a/lib/Migration/Version21000Date20241212134329.php b/lib/Migration/Version21000Date20241212134329.php new file mode 100644 index 00000000000..48a652a97f0 --- /dev/null +++ b/lib/Migration/Version21000Date20241212134329.php @@ -0,0 +1,39 @@ +getTable('talk_rooms'); + if (!$table->hasColumn('encrypted')) { + $table->addColumn('encrypted', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => true, + ]); + } + + return $schema; + } +} diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 14f5dc36f96..d1d54c3d088 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -20,6 +20,7 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ->addSelect($alias . 'read_only') ->addSelect($alias . 'lobby_state') ->addSelect($alias . 'sip_enabled') + ->addSelect($alias . 'encrypted') ->addSelect($alias . 'assigned_hpb') ->addSelect($alias . 'token') ->addSelect($alias . 'name') diff --git a/lib/Room.php b/lib/Room.php index aa5ae0b02f7..a99eafdcd35 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -103,6 +103,7 @@ public function __construct( private int $messageExpiration, private int $lobbyState, private int $sipEnabled, + private bool $encryptionEnabled, private ?int $assignedSignalingServer, private string $token, private string $name, @@ -223,6 +224,14 @@ public function setSIPEnabled(int $sipEnabled): void { $this->sipEnabled = $sipEnabled; } + public function getEncryptionEnabled(): bool { + return $this->encryptionEnabled; + } + + public function setEncryptionEnabled(bool $encryptionEnabled): void { + $this->encryptionEnabled = $encryptionEnabled; + } + public function getAssignedSignalingServer(): ?int { return $this->assignedSignalingServer; } @@ -432,6 +441,7 @@ public function getPropertiesForSignaling(string $userId, bool $roomModified = t 'listable' => $this->getListable(), 'active-since' => $this->getActiveSince(), 'sip-enabled' => $this->getSIPEnabled(), + 'encrypted' => $this->getEncryptionEnabled(), ]; if ($roomModified) { diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 22fcff9852d..0614e49e50e 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -119,6 +119,7 @@ public function formatRoomV4( 'lastPing' => 0, 'sessionId' => '0', 'sipEnabled' => Webinary::SIP_DISABLED, + 'encrypted' => false, 'actorType' => '', 'actorId' => '', 'attendeeId' => 0, @@ -177,6 +178,7 @@ public function formatRoomV4( 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, 'sipEnabled' => $room->getSIPEnabled(), + 'encrypted' => $room->getEncryptionEnabled(), 'listable' => $room->getListable(), 'breakoutRoomMode' => $room->getBreakoutRoomMode(), 'breakoutRoomStatus' => $room->getBreakoutRoomStatus(), @@ -210,6 +212,7 @@ public function formatRoomV4( 'notificationCalls' => $attendee->getNotificationCalls(), 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, + 'encrypted' => $room->getEncryptionEnabled(), 'actorType' => $attendee->getActorType(), 'actorId' => $attendee->getActorId(), 'attendeeId' => $attendee->getId(), diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 9d0632c6902..63e6eb0038f 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -298,6 +298,28 @@ public function setSIPEnabled(Room $room, int $newSipEnabled): void { $this->dispatcher->dispatchTyped($event); } + public function setEncryptionEnabled(Room $room, bool $newEncryptionEnabled): void { + $oldEncryptionEnabled = $room->getEncryptionEnabled(); + + if ($newEncryptionEnabled === $oldEncryptionEnabled) { + return; + } + + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED, $newEncryptionEnabled, $oldEncryptionEnabled); + $this->dispatcher->dispatchTyped($event); + + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set('encrypted', $update->createNamedParameter($newEncryptionEnabled, IQueryBuilder::PARAM_BOOL)) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + $room->setEncryptionEnabled($newEncryptionEnabled); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED, $newEncryptionEnabled, $oldEncryptionEnabled); + $this->dispatcher->dispatchTyped($event); + } + /** * @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent * @throws RecordingConsentException When the room has an active call or the value is invalid diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index f2bddcd5899..07108cc5e4b 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -59,6 +59,7 @@ class Listener implements IEventListener { ARoomModifiedEvent::PROPERTY_PASSWORD, ARoomModifiedEvent::PROPERTY_READ_ONLY, ARoomModifiedEvent::PROPERTY_SIP_ENABLED, + ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED, ARoomModifiedEvent::PROPERTY_TYPE, ]; diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 1551163f402..31b40e13ac4 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -49,6 +49,7 @@ :name="t('spreed', 'Meeting')"> + @@ -129,6 +130,7 @@ import MatterbridgeSettings from './Matterbridge/MatterbridgeSettings.vue' import MentionsSettings from './MentionsSettings.vue' import NotificationsSettings from './NotificationsSettings.vue' import RecordingConsentSettings from './RecordingConsentSettings.vue' +import SecuritySettings from './SecuritySettings.vue' import SipSettings from './SipSettings.vue' import { CALL, CONFIG, PARTICIPANT, CONVERSATION } from '../../constants.js' @@ -159,6 +161,7 @@ export default { NcCheckboxRadioSwitch, NotificationsSettings, RecordingConsentSettings, + SecuritySettings, SipSettings, }, diff --git a/src/components/ConversationSettings/SecuritySettings.vue b/src/components/ConversationSettings/SecuritySettings.vue new file mode 100644 index 00000000000..806c8f3d560 --- /dev/null +++ b/src/components/ConversationSettings/SecuritySettings.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index 1a595acef8d..b91f9b3b23f 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -255,6 +255,18 @@ const setSIPEnabled = async function(token, newState) { }) } +/** + * Change the end-to-end encryption enabled + * + * @param {string} token The token of the conversation to be modified + * @param {boolean} newState The new enabled state to set + */ +const setEncryptionEnabled = async function(token, newState) { + return axios.put(generateOcsUrl('apps/spreed/api/v4/room/{token}/webinar/encryption', { token }), { + enabled: newState, + }) +} + /** * Change the recording consent per conversation * @@ -372,6 +384,7 @@ export { makeConversationPublic, makeConversationPrivate, setSIPEnabled, + setEncryptionEnabled, setRecordingConsent, changeLobbyState, changeReadOnlyState, diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index 76979c6556e..d6d42909b8a 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -27,6 +27,7 @@ import { makeConversationPublic, makeConversationPrivate, setSIPEnabled, + setEncryptionEnabled, setRecordingConsent, changeLobbyState, changeReadOnlyState, @@ -684,6 +685,20 @@ const actions = { } }, + async setEncryptionEnabled({ commit, getters }, { token, enabled }) { + if (!getters.conversations[token]) { + return + } + + try { + await setEncryptionEnabled(token, enabled) + const conversation = Object.assign({}, getters.conversations[token], { encrypted: enabled }) + commit('addConversation', conversation) + } catch (error) { + console.error('Error while changing the encryption state for conversation: ', error) + } + }, + async setRecordingConsent({ commit, getters }, { token, state }) { if (!getters.conversations[token]) { return diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 9f2bd012831..eb9360d34a4 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -759,6 +759,10 @@ Signaling.Standalone.prototype.connect = function() { this.currentRoomToken = null this.nextcloudSessionId = null } else { + if (this.currentRoomToken && data.room.roomid === this.currentRoomToken) { + this._trigger('roomEncryption', [data.room.roomid, data.room.properties.encrypted || false]) + } + // TODO(fancycode): Only fetch properties of room that was modified. EventBus.emit('should-refresh-conversations') } diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index 4b6934bd3fa..11a59ae554d 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -96,6 +96,16 @@ async function signalingGetSettingsForRecording(token, random, checksum) { return getSignalingSettings(token, options) } +/** + * Update the encryption module. + * + * @param {boolean} encrypted True if encryption should be enabled, false otherwise. + */ +async function updateEncryption(encrypted) { + console.debug('Setup encryption', encrypted) + // TODO: Setup end-to-end encryption depending on "encryped" flag. +} + /** * @param {string} token The token of the conversation to connect to */ @@ -122,6 +132,11 @@ async function connectSignaling(token) { const settings = await getSignalingSettings(token) console.debug('Received updated settings', settings) signaling.setSettings(settings) + await updateEncryption(settings.encrypted) + }) + + signaling.on('roomEncryption', async function(roomId, encrypted) { + await updateEncryption(encrypted) }) signalingTypingHandler?.setSignaling(signaling) @@ -129,6 +144,8 @@ async function connectSignaling(token) { signaling.setSettings(settings) } + await updateEncryption(settings.encrypted) + tokensInSignaling[token] = true }