feat: Add option to disable end-to-end encryption to allow legacy clients.

Signed-off-by: Joachim Bauch <bauch@struktur.de>
This commit is contained in:
Joachim Bauch
2024-12-16 10:14:58 +01:00
parent 22f237400d
commit 4fb8481492
17 changed files with 251 additions and 4 deletions

View File

@ -103,6 +103,8 @@ return [
['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() */

View File

@ -2369,6 +2369,35 @@ class RoomController extends AEnvironmentAwareOCSController {
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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_FORBIDDEN|Http::STATUS_PRECONDITION_FAILED, array{error: 'config'}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'token'|'type'|'value'}, array{}>
*
* 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
*

View File

@ -218,6 +218,7 @@ class SignalingController extends OCSController {
'stunservers' => $stun,
'turnservers' => $turn,
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
'encrypted' => $room->getEncryptionEnabled(),
];
return new DataResponse($data);

View File

@ -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 @@ abstract class ARoomModifiedEvent extends ARoomEvent {
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;
}

View File

@ -98,6 +98,7 @@ class Manager {
'message_expiration' => 0,
'lobby_state' => 0,
'sip_enabled' => 0,
'encrypted' => false,
'assigned_hpb' => null,
'token' => '',
'name' => '',
@ -167,6 +168,7 @@ class Manager {
(int)$row['message_expiration'],
(int)$row['lobby_state'],
(int)$row['sip_enabled'],
(bool)$row['encrypted'],
$assignedSignalingServer,
(string)$row['token'],
(string)$row['name'],

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version21000Date20241212134329 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('talk_rooms');
if (!$table->hasColumn('encrypted')) {
$table->addColumn('encrypted', Types::BOOLEAN, [
'notnull' => false,
'default' => true,
]);
}
return $schema;
}
}

View File

@ -20,6 +20,7 @@ class SelectHelper {
->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')

View File

@ -103,6 +103,7 @@ class Room {
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 @@ class Room {
$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 @@ class Room {
'listable' => $this->getListable(),
'active-since' => $this->getActiveSince(),
'sip-enabled' => $this->getSIPEnabled(),
'encrypted' => $this->getEncryptionEnabled(),
];
if ($roomModified) {

View File

@ -119,6 +119,7 @@ class RoomFormatter {
'lastPing' => 0,
'sessionId' => '0',
'sipEnabled' => Webinary::SIP_DISABLED,
'encrypted' => false,
'actorType' => '',
'actorId' => '',
'attendeeId' => 0,
@ -177,6 +178,7 @@ class RoomFormatter {
'lobbyState' => $room->getLobbyState(),
'lobbyTimer' => $lobbyTimer,
'sipEnabled' => $room->getSIPEnabled(),
'encrypted' => $room->getEncryptionEnabled(),
'listable' => $room->getListable(),
'breakoutRoomMode' => $room->getBreakoutRoomMode(),
'breakoutRoomStatus' => $room->getBreakoutRoomStatus(),
@ -210,6 +212,7 @@ class RoomFormatter {
'notificationCalls' => $attendee->getNotificationCalls(),
'lobbyState' => $room->getLobbyState(),
'lobbyTimer' => $lobbyTimer,
'encrypted' => $room->getEncryptionEnabled(),
'actorType' => $attendee->getActorType(),
'actorId' => $attendee->getActorId(),
'attendeeId' => $attendee->getId(),

View File

@ -298,6 +298,28 @@ class RoomService {
$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

View File

@ -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,
];

View File

@ -49,6 +49,7 @@
:name="t('spreed', 'Meeting')">
<LobbySettings :token="token" />
<SipSettings v-if="canUserEnableSIP" />
<SecuritySettings />
</NcAppSettingsSection>
<!-- Conversation permissions -->
@ -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,
},

View File

@ -0,0 +1,84 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="app-settings-subsection">
<h4 class="app-settings-section__subtitle">
{{ t('spreed', 'Security') }}
</h4>
<div>
<NcCheckboxRadioSwitch :checked="!hasEncryptionEnabled"
type="switch"
aria-describedby="encryption_settings_hint"
:disabled="isEncryptionLoading"
@update:checked="toggleSetting()">
{{ t('spreed', 'Disable end-to-end encryption to allow legacy clients.') }}
</NcCheckboxRadioSwitch>
</div>
</div>
</template>
<script>
import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
export default {
name: 'SecuritySettings',
components: {
NcCheckboxRadioSwitch,
},
data() {
return {
isEncryptionLoading: false,
}
},
computed: {
token() {
return this.$store.getters.getToken()
},
conversation() {
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
},
hasEncryptionEnabled() {
return this.conversation.encrypted || false
},
},
methods: {
t,
async toggleSetting() {
const enabled = !this.conversation.encrypted
try {
await this.$store.dispatch('setEncryptionEnabled', {
token: this.token,
enabled,
})
if (this.conversation.encrypted) {
showSuccess(t('spreed', 'End-to-end encryption is now enabled'))
} else {
showSuccess(t('spreed', 'End-to-end encryption is now disabled'))
}
} catch (e) {
// TODO check "precondition failed"
if (!this.conversation.encrypted) {
console.error('Error occurred when enabling end-to-end encryption', e)
showError(t('spreed', 'Error occurred when enabling end-to-end encryption'))
} else {
console.error('Error occurred when disabling end-to-end encryption', e)
showError(t('spreed', 'Error occurred when disabling end-to-end encryption'))
}
}
},
},
}
</script>

View File

@ -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,

View File

@ -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

View File

@ -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')
}

View File

@ -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
}