diff --git a/docs/capabilities.md b/docs/capabilities.md
index c0e0e7b4d63..3f68c95d6cf 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -175,3 +175,5 @@
* `config => chat => has-translation-task-providers` (local) - When true, translations can be done using the [OCS TaskProcessing API](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-taskprocessing-api.html).
* `config => conversations => list-style` (local) - Whether conversation list should appear in certain way
* `config => conversations => description-length` (local) - The maximum length for conversation descriptions, currently 2000. Before this config was added the implicit limit was 500, since the existance of the feature capability `room-description`.
+* `call-end-to-end-encryption` - Signaling support of the server for the end-to-end encryption of calls
+* `config => call => end-to-end-encryption` - Whether calls should be end-to-end encrypted (currently off by default, until all Talk mobile clients support it)
diff --git a/docs/settings.md b/docs/settings.md
index b1924aee7f8..05d97a08e24 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -118,5 +118,6 @@ Legend:
| `enable_matterbridge` | string
`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured |
| `force_passwords` | string
`1` or `0` | `0` | No | ️ | Whether public chats are forced to use a password |
| `create_samples` | string
`1` or `0` | `1` | No | ️ | Create sample conversations (the content can be overwritten by providing files in a provided `samples_directory` app config) |
+| `call_end_to_end_encryption` | string
`1` or `0` | `0` | No | 🖌️ | Whether clients should end-to-end encrypt streams in calls (Only supported with High-performance backend) |
| `inactivity_lock_after_days` | int | `0` | No | | A duration (in days) after which rooms are locked. Calculated from the last activity in the room. |
| `inactivity_enable_lobby` | string
`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. |
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 3f7a4c1cb8f..af716e51757 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -119,6 +119,7 @@ class Capabilities implements IPublicCapability {
'message-expiration',
'reactions',
'chat-summary-api',
+ 'call-end-to-end-encryption',
];
public const LOCAL_FEATURES = [
@@ -224,6 +225,7 @@ public function getCapabilities(): array {
'start-without-media' => $this->talkConfig->getCallsStartWithoutMedia($user?->getUID()),
'max-duration' => $this->appConfig->getAppValueInt('max_call_duration'),
'blur-virtual-background' => $this->talkConfig->getBlurVirtualBackground($user?->getUID()),
+ 'end-to-end-encryption' => $this->talkConfig->isCallEndToEndEncryptionEnabled(),
],
'chat' => [
'max-length' => ChatManager::MAX_CHAT_LENGTH,
@@ -337,6 +339,10 @@ public function getCapabilities(): array {
$capabilities['config']['chat']['has-translation-task-providers'] = true;
}
+ if ($this->talkConfig->getSignalingMode() === Config::SIGNALING_EXTERNAL) {
+ $capabilities['features'][] = 'call-end-to-end-encryption';
+ }
+
return [
'spreed' => $capabilities,
];
diff --git a/lib/Config.php b/lib/Config.php
index d0f40a60f1d..461890e7096 100644
--- a/lib/Config.php
+++ b/lib/Config.php
@@ -732,4 +732,13 @@ public function enableLobbyOnLockedRooms(): bool {
public function isPasswordEnforced(): bool {
return $this->appConfig->getAppValueBool('force_passwords');
}
+
+ public function isCallEndToEndEncryptionEnabled(): bool {
+ if ($this->getSignalingMode() !== self::SIGNALING_EXTERNAL) {
+ return false;
+ }
+
+ // TODO Default value will be set to true, once all mobile clients support it.
+ return $this->appConfig->getAppValueBool('call_end_to_end_encryption');
+ }
}
diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index 64892a9a679..52cbb7fd5f1 100644
--- a/lib/Controller/SignalingController.php
+++ b/lib/Controller/SignalingController.php
@@ -8,7 +8,6 @@
namespace OCA\Talk\Controller;
-use GuzzleHttp\Exception\ConnectException;
use OCA\Talk\Config;
use OCA\Talk\Events\BeforeSignalingResponseSentEvent;
use OCA\Talk\Exceptions\ForbiddenException;
@@ -22,7 +21,6 @@
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCA\Talk\Service\BanService;
-use OCA\Talk\Service\CertificateService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Signaling\Messages;
@@ -59,7 +57,6 @@ public function __construct(
private \OCA\Talk\Signaling\Manager $signalingManager,
private TalkSession $session,
private Manager $manager,
- private CertificateService $certificateService,
private ParticipantService $participantService,
private SessionService $sessionService,
private IDBConnection $dbConnection,
@@ -272,89 +269,12 @@ private function getFederationSettings(?Room $room): ?array {
*/
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION, tags: ['settings'])]
public function getWelcomeMessage(int $serverId): DataResponse {
- $signalingServers = $this->talkConfig->getSignalingServers();
- if (empty($signalingServers) || !isset($signalingServers[$serverId])) {
- return new DataResponse(null, Http::STATUS_NOT_FOUND);
- }
-
- $url = rtrim($signalingServers[$serverId]['server'], '/');
- $url = strtolower($url);
-
- if (str_starts_with($url, 'wss://')) {
- $url = 'https://' . substr($url, 6);
- }
-
- if (str_starts_with($url, 'ws://')) {
- $url = 'http://' . substr($url, 5);
- }
-
- $verifyServer = (bool)$signalingServers[$serverId]['verify'];
-
- if ($verifyServer && str_contains($url, 'https://')) {
- $expiration = $this->certificateService->getCertificateExpirationInDays($url);
-
- if ($expiration < 0) {
- return new DataResponse(['error' => 'CERTIFICATE_EXPIRED'], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
- }
-
- $client = $this->clientService->newClient();
try {
- $timeBefore = $this->timeFactory->getTime();
- $response = $client->get($url . '/api/v1/welcome', [
- 'verify' => $verifyServer,
- 'nextcloud' => [
- 'allow_local_address' => true,
- ],
- ]);
- $timeAfter = $this->timeFactory->getTime();
-
- $body = $response->getBody();
- $data = json_decode($body, true);
-
- if (!is_array($data)) {
- return new DataResponse([
- 'error' => 'JSON_INVALID',
- ], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- if (!isset($data['version'])) {
- return new DataResponse([
- 'error' => 'UPDATE_REQUIRED',
- 'version' => '',
- ], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- if (!$this->signalingManager->isCompatibleSignalingServer($response)) {
- return new DataResponse([
- 'error' => 'UPDATE_REQUIRED',
- 'version' => $data['version'] ?? '',
- ], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- $responseTime = $this->timeFactory->getDateTime($response->getHeader('date'))->getTimestamp();
- if (($timeBefore - Config::ALLOWED_BACKEND_TIMEOFFSET) > $responseTime
- || ($timeAfter + Config::ALLOWED_BACKEND_TIMEOFFSET) < $responseTime) {
- return new DataResponse([
- 'error' => 'TIME_OUT_OF_SYNC',
- ], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- $missingFeatures = $this->signalingManager->getSignalingServerMissingFeatures($response);
- if (!empty($missingFeatures)) {
- return new DataResponse([
- 'warning' => 'UPDATE_OPTIONAL',
- 'features' => $missingFeatures,
- 'version' => $data['version'] ?? '',
- ]);
- }
-
- return new DataResponse($data);
- } catch (ConnectException $e) {
- return new DataResponse(['error' => 'CAN_NOT_CONNECT'], Http::STATUS_INTERNAL_SERVER_ERROR);
- } catch (\Exception $e) {
- return new DataResponse(['error' => (string)$e->getCode()], Http::STATUS_INTERNAL_SERVER_ERROR);
+ $testResult = $this->signalingManager->checkServerCompatibility($serverId);
+ } catch (\OutOfBoundsException) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
+ return new DataResponse($testResult['data'], $testResult['status']);
}
/**
diff --git a/lib/Middleware/CanUseTalkMiddleware.php b/lib/Middleware/CanUseTalkMiddleware.php
index fd01d305bb0..f9dcaf9eedc 100644
--- a/lib/Middleware/CanUseTalkMiddleware.php
+++ b/lib/Middleware/CanUseTalkMiddleware.php
@@ -36,9 +36,11 @@ class CanUseTalkMiddleware extends Middleware {
public const TALK_DESKTOP_MIN_VERSION_RECORDING_CONSENT = '0.16.0';
public const TALK_ANDROID_MIN_VERSION = '15.0.0';
public const TALK_ANDROID_MIN_VERSION_RECORDING_CONSENT = '18.0.0';
+ public const TALK_ANDROID_MIN_VERSION_E2EE_CALLS = '22.0.0';
public const TALK_IOS_MIN_VERSION = '15.0.0';
public const TALK_IOS_MIN_VERSION_RECORDING_CONSENT = '18.0.0';
+ public const TALK_IOS_MIN_VERSION_E2EE_CALLS = '22.0.0';
public function __construct(
@@ -139,14 +141,18 @@ protected function throwIfUnsupportedClientVersion(string $client, string $userA
$versionRegex = IRequest::USER_AGENT_TALK_ANDROID;
$minVersion = self::TALK_ANDROID_MIN_VERSION;
- if ($this->talkConfig->recordingConsentRequired()) {
+ if ($this->talkConfig->isCallEndToEndEncryptionEnabled()) {
+ $minVersion = self::TALK_ANDROID_MIN_VERSION_E2EE_CALLS;
+ } elseif ($this->talkConfig->recordingConsentRequired()) {
$minVersion = self::TALK_ANDROID_MIN_VERSION_RECORDING_CONSENT;
}
} elseif ($client === 'ios') {
$versionRegex = IRequest::USER_AGENT_TALK_IOS;
$minVersion = self::TALK_IOS_MIN_VERSION;
- if ($this->talkConfig->recordingConsentRequired()) {
+ if ($this->talkConfig->isCallEndToEndEncryptionEnabled()) {
+ $minVersion = self::TALK_IOS_MIN_VERSION_E2EE_CALLS;
+ } elseif ($this->talkConfig->recordingConsentRequired()) {
$minVersion = self::TALK_IOS_MIN_VERSION_RECORDING_CONSENT;
}
} else {
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index e2016a5a515..90880bb0e00 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -347,6 +347,7 @@
* start-without-media: bool,
* max-duration: int,
* blur-virtual-background: bool,
+ * end-to-end-encryption: bool,
* },
* chat: array{
* max-length: int,
diff --git a/lib/SetupCheck/HighPerformanceBackend.php b/lib/SetupCheck/HighPerformanceBackend.php
index 030b0d6f257..9470f0a5d25 100644
--- a/lib/SetupCheck/HighPerformanceBackend.php
+++ b/lib/SetupCheck/HighPerformanceBackend.php
@@ -9,6 +9,8 @@
namespace OCA\Talk\SetupCheck;
use OCA\Talk\Config;
+use OCA\Talk\Signaling\Manager;
+use OCP\AppFramework\Http;
use OCP\ICacheFactory;
use OCP\IL10N;
use OCP\IURLGenerator;
@@ -21,6 +23,7 @@ public function __construct(
readonly protected ICacheFactory $cacheFactory,
readonly protected IURLGenerator $urlGenerator,
readonly protected IL10N $l,
+ readonly protected Manager $signalManager,
) {
}
@@ -54,12 +57,58 @@ public function run(): SetupResult {
);
}
- if ($this->cacheFactory->isAvailable()) {
- return SetupResult::success();
+ try {
+ $testResult = $this->signalManager->checkServerCompatibility(0);
+ } catch (\OutOfBoundsException) {
+ return SetupResult::error($this->l->t('High-performance backend not configured correctly'));
}
- return SetupResult::warning(
- $this->l->t('It is highly recommended to configure a memory cache when running Nextcloud Talk with a High-performance backend.'),
- $this->urlGenerator->linkToDocs('admin-cache'),
- );
+ if ($testResult['status'] === Http::STATUS_INTERNAL_SERVER_ERROR) {
+ $error = $testResult['data']['error'];
+ if ($error === 'CAN_NOT_CONNECT') {
+ return SetupResult::error($this->l->t('Error: Cannot connect to server'));
+ }
+ if ($error === 'JSON_INVALID') {
+ return SetupResult::error($this->l->t('Error: Server did not respond with proper JSON'));
+ }
+ if ($error === 'CERTIFICATE_EXPIRED') {
+ return SetupResult::error($this->l->t('Error: Certificate expired'));
+ }
+ if ($error === 'TIME_OUT_OF_SYNC') {
+ return SetupResult::error($this->l->t('Error: System times of Nextcloud server and High-performance backend server are out of sync. Please make sure that both servers are connected to a time-server or manually synchronize their time.'));
+ }
+ if ($error === 'UPDATE_REQUIRED') {
+ $version = $testResult['data']['version'] ?? $this->l->t('Could not get version');
+ return SetupResult::error(str_replace(
+ '{version}',
+ $version,
+ $this->l->t('Error: Running version: {version}; Server needs to be updated to be compatible with this version of Talk'),
+ ));
+ }
+ if ($error) {
+ return SetupResult::error(str_replace('{error}', $error, $this->l->t('Error: Server responded with: {error}')));
+ }
+ return SetupResult::error($this->l->t('Error: Unknown error occurred'));
+ }
+ if ($testResult['status'] === Http::STATUS_OK
+ && isset($testResult['data']['warning'])
+ && $testResult['data']['warning'] === 'UPDATE_OPTIONAL'
+ ) {
+ $version = $testResult['data']['version'] ?? $this->l->t('Could not get version');
+ $features = implode(', ', $testResult['data']['features'] ?? []);
+ return SetupResult::warning(str_replace(
+ ['{version}', '{features}'],
+ [$version, $features],
+ $this->l->t('Warning: Running version: {version}; Server does not support all features of this Talk version, missing features: {features}')
+ ));
+ }
+
+ if (!$this->cacheFactory->isAvailable()) {
+ return SetupResult::warning(
+ $this->l->t('It is highly recommended to configure a memory cache when running Nextcloud Talk with a High-performance backend.'),
+ $this->urlGenerator->linkToDocs('admin-cache'),
+ );
+ }
+
+ return SetupResult::success();
}
}
diff --git a/lib/Signaling/Manager.php b/lib/Signaling/Manager.php
index c795f7540c7..61e36cce656 100644
--- a/lib/Signaling/Manager.php
+++ b/lib/Signaling/Manager.php
@@ -8,10 +8,15 @@
namespace OCA\Talk\Signaling;
+use GuzzleHttp\Exception\ConnectException;
use OCA\Talk\CachePrefix;
use OCA\Talk\Config;
use OCA\Talk\Room;
+use OCA\Talk\Service\CertificateService;
use OCA\Talk\Service\RoomService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -26,11 +31,139 @@ public function __construct(
protected IConfig $serverConfig,
protected Config $talkConfig,
protected RoomService $roomService,
+ protected ITimeFactory $timeFactory,
+ protected IClientService $clientService,
+ protected CertificateService $certificateService,
ICacheFactory $cacheFactory,
) {
$this->cache = $cacheFactory->createDistributed(CachePrefix::SIGNALING_ASSIGNED_SERVER);
}
+ /**
+ * @param int $serverId
+ * @return array{status: Http::STATUS_OK, data: array}|array{status: Http::STATUS_INTERNAL_SERVER_ERROR, data: array{error: string, version?: string}}
+ * @throws \OutOfBoundsException When the serverId is not found
+ */
+ public function checkServerCompatibility(int $serverId): array {
+ $signalingServers = $this->talkConfig->getSignalingServers();
+ if (empty($signalingServers) || !isset($signalingServers[$serverId])) {
+ throw new \OutOfBoundsException();
+ }
+
+ $url = rtrim($signalingServers[$serverId]['server'], '/');
+ $url = strtolower($url);
+
+ if (str_starts_with($url, 'wss://')) {
+ $url = 'https://' . substr($url, 6);
+ }
+
+ if (str_starts_with($url, 'ws://')) {
+ $url = 'http://' . substr($url, 5);
+ }
+
+ $verifyServer = (bool)$signalingServers[$serverId]['verify'];
+
+ if ($verifyServer && str_contains($url, 'https://')) {
+ $expiration = $this->certificateService->getCertificateExpirationInDays($url);
+
+ if ($expiration < 0) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'CERTIFICATE_EXPIRED',
+ ],
+ ];
+ }
+ }
+
+ $client = $this->clientService->newClient();
+ try {
+ $timeBefore = $this->timeFactory->getTime();
+ $response = $client->get($url . '/api/v1/welcome', [
+ 'verify' => $verifyServer,
+ 'nextcloud' => [
+ 'allow_local_address' => true,
+ ],
+ ]);
+ $timeAfter = $this->timeFactory->getTime();
+
+ $body = $response->getBody();
+ $data = json_decode($body, true);
+
+ if (!is_array($data)) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'JSON_INVALID',
+ ],
+ ];
+ }
+
+ if (!isset($data['version'])) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'UPDATE_REQUIRED',
+ 'version' => '',
+ ],
+ ];
+ }
+
+ if (!$this->isCompatibleSignalingServer($response)) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'UPDATE_REQUIRED',
+ 'version' => $data['version'] ?? '',
+ ],
+ ];
+ }
+
+ $responseTime = $this->timeFactory->getDateTime($response->getHeader('date'))->getTimestamp();
+ if (($timeBefore - Config::ALLOWED_BACKEND_TIMEOFFSET) > $responseTime
+ || ($timeAfter + Config::ALLOWED_BACKEND_TIMEOFFSET) < $responseTime) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'TIME_OUT_OF_SYNC',
+ ],
+ ];
+ }
+
+ $missingFeatures = $this->getSignalingServerMissingFeatures($response);
+ if (!empty($missingFeatures)) {
+ return [
+ 'status' => Http::STATUS_OK,
+ 'data' => [
+ 'warning' => 'UPDATE_OPTIONAL',
+ 'features' => $missingFeatures,
+ 'version' => $data['version'],
+ ],
+ ];
+ }
+
+ return [
+ 'status' => Http::STATUS_OK,
+ 'data' => $data,
+ ];
+ } catch (ConnectException) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => 'CAN_NOT_CONNECT',
+ ],
+ ];
+ } catch (\Exception $e) {
+ return [
+ 'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
+ 'data' => [
+ 'error' => (string)$e->getCode(),
+ ],
+ ];
+ }
+
+ }
+
public function isCompatibleSignalingServer(IResponse $response): bool {
$featureHeader = $response->getHeader(self::FEATURE_HEADER);
$features = explode(',', $featureHeader);
@@ -49,6 +182,7 @@ public function getSignalingServerMissingFeatures(IResponse $response): array {
return array_values(array_diff([
'dialout',
+ 'join-features',
], $features));
}
diff --git a/openapi-administration.json b/openapi-administration.json
index c05940b78b1..3dc667a4122 100644
--- a/openapi-administration.json
+++ b/openapi-administration.json
@@ -150,7 +150,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -199,6 +200,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-backend-recording.json b/openapi-backend-recording.json
index 20030f63c7f..571ea282643 100644
--- a/openapi-backend-recording.json
+++ b/openapi-backend-recording.json
@@ -83,7 +83,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -132,6 +133,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-backend-signaling.json b/openapi-backend-signaling.json
index 2a3e15d6886..a719b7a020d 100644
--- a/openapi-backend-signaling.json
+++ b/openapi-backend-signaling.json
@@ -83,7 +83,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -132,6 +133,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json
index 0faed9c123b..f078c3cc06c 100644
--- a/openapi-backend-sipbridge.json
+++ b/openapi-backend-sipbridge.json
@@ -126,7 +126,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -175,6 +176,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-bots.json b/openapi-bots.json
index 8f65778056e..fc98590d7b0 100644
--- a/openapi-bots.json
+++ b/openapi-bots.json
@@ -83,7 +83,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -132,6 +133,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-federation.json b/openapi-federation.json
index 03b18eff9d7..1e9b5d4d18b 100644
--- a/openapi-federation.json
+++ b/openapi-federation.json
@@ -126,7 +126,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -175,6 +176,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi-full.json b/openapi-full.json
index 94a0354eeb1..5af06fb7e87 100644
--- a/openapi-full.json
+++ b/openapi-full.json
@@ -284,7 +284,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -333,6 +334,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/openapi.json b/openapi.json
index c76ab8c9242..5cde9573d47 100644
--- a/openapi.json
+++ b/openapi.json
@@ -243,7 +243,8 @@
"can-enable-sip",
"start-without-media",
"max-duration",
- "blur-virtual-background"
+ "blur-virtual-background",
+ "end-to-end-encryption"
],
"properties": {
"enabled": {
@@ -292,6 +293,9 @@
},
"blur-virtual-background": {
"type": "boolean"
+ },
+ "end-to-end-encryption": {
+ "type": "boolean"
}
}
},
diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts
index 6ab75901a68..eaca66b63db 100644
--- a/src/__mocks__/capabilities.ts
+++ b/src/__mocks__/capabilities.ts
@@ -91,6 +91,11 @@ export const mockedCapabilities: Capabilities = {
'conversation-creation-password',
'call-notification-state-api',
'schedule-meeting',
+ // Conditional features
+ 'message-expiration',
+ 'reactions',
+ 'chat-summary-api',
+ 'call-end-to-end-encryption',
],
'features-local': [
'favorites',
@@ -126,6 +131,7 @@ export const mockedCapabilities: Capabilities = {
'start-without-media': false,
'max-duration': 0,
'blur-virtual-background': false,
+ 'end-to-end-encryption': false,
},
chat: {
'max-length': 32000,
@@ -176,6 +182,7 @@ export const mockedCapabilities: Capabilities = {
conversations: [
'can-create',
'list-style',
+ 'description-length',
],
federation: [
'enabled',
diff --git a/src/components/AdminSettings/GeneralSettings.vue b/src/components/AdminSettings/GeneralSettings.vue
index 05244a4b5fc..1cbd37e28fa 100644
--- a/src/components/AdminSettings/GeneralSettings.vue
+++ b/src/components/AdminSettings/GeneralSettings.vue
@@ -27,15 +27,38 @@
{{ t('spreed', 'Allow conversations on files') }}
{{ t('spreed', 'Allow conversations on public shares for files') }}
+
+
+
+ {{ t('spreed', 'End-to-end encrypted calls') }}
+ {{ t('spreed', 'Beta') }}
+
+
+
+ {{ t('spreed', 'Enable encryption') }}
+
+
+
+
+
@@ -44,8 +67,11 @@ import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import { EventBus } from '../../services/EventBus.ts'
+
const defaultGroupNotificationOptions = [
{ value: 1, label: t('spreed', 'All messages') },
{ value: 2, label: t('spreed', '@-mentions only') },
@@ -55,6 +81,7 @@ export default {
name: 'GeneralSettings',
components: {
+ NcNoteCard,
NcCheckboxRadioSwitch,
NcSelect,
},
@@ -70,6 +97,11 @@ export default {
conversationsFiles: parseInt(loadState('spreed', 'conversations_files')) === 1,
conversationsFilesPublicShares: parseInt(loadState('spreed', 'conversations_files_public_shares')) === 1,
+
+ hasFeatureJoinFeatures: false,
+ hasSignalingServers: false,
+ isE2EECallsEnabled: false,
+ hasSIPBridge: !!loadState('spreed', 'sip_bridge_shared_secret'),
}
},
@@ -80,16 +112,53 @@ export default {
isConversationsFilesPublicSharesChecked() {
return this.conversationsFilesPublicShares
},
+ canEnableE2EECalls() {
+ return this.hasFeatureJoinFeatures || !this.hasSIPBridge
+ },
},
mounted() {
this.loading = true
this.defaultGroupNotification = defaultGroupNotificationOptions[parseInt(loadState('spreed', 'default_group_notification')) - 1]
this.loading = false
+
+ const signaling = loadState('spreed', 'signaling_servers')
+ this.updateSignalingServers(signaling.servers)
+ EventBus.on('signaling-servers-updated', this.updateSignalingServers)
+ EventBus.on('signaling-server-connected', this.updateSignalingDetails)
+ EventBus.on('sip-settings-updated', this.updateSipDetails)
+ },
+
+ beforeDestroy() {
+ EventBus.off('signaling-servers-updated', this.updateSignalingServers)
+ EventBus.off('signaling-server-connected', this.updateSignalingDetails)
+ EventBus.off('sip-settings-updated', this.updateSipDetails)
},
methods: {
t,
+
+ updateSignalingServers(servers) {
+ this.hasSignalingServers = servers.length > 0
+ },
+
+ updateSignalingDetails(signaling) {
+ this.hasFeatureJoinFeatures = signaling.hasFeature('join-features')
+ },
+
+ updateSipDetails(settings) {
+ this.hasSIPBridge = !!settings.sharedSecret
+ },
+
+ updateE2EECallsEnabled(value) {
+ this.loading = true
+ OCP.AppConfig.setValue('spreed', 'call_end_to_end_encryption', value ? '1' : '0', {
+ success: () => {
+ this.loading = false
+ },
+ })
+ },
+
saveDefaultGroupNotification() {
this.loadingDefaultGroupNotification = true
@@ -139,6 +208,13 @@ h3 {
font-weight: 600;
}
+small {
+ color: var(--color-warning);
+ border: 1px solid var(--color-warning);
+ border-radius: 16px;
+ padding: 0 9px;
+}
+
.default-group-notification {
min-width: 300px !important;
}
diff --git a/src/components/AdminSettings/SIPBridge.vue b/src/components/AdminSettings/SIPBridge.vue
index 70c7be5460f..39f4455cf82 100644
--- a/src/components/AdminSettings/SIPBridge.vue
+++ b/src/components/AdminSettings/SIPBridge.vue
@@ -188,6 +188,7 @@ export default {
dialOutEnabled: this.dialOutEnabled,
sipGroups: this.sipGroups.map(group => group.id).join('_')
}
+ EventBus.emit('sip-settings-updated', this.currentSetup)
},
async saveSIPSettings() {
diff --git a/src/components/AdminSettings/SignalingServer.vue b/src/components/AdminSettings/SignalingServer.vue
index e356b471e69..6a2f8c4bf2a 100644
--- a/src/components/AdminSettings/SignalingServer.vue
+++ b/src/components/AdminSettings/SignalingServer.vue
@@ -77,6 +77,7 @@ import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadi
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import { EventBus } from '../../services/EventBus.ts'
import { fetchSignalingSettings, getWelcomeMessage } from '../../services/signalingService.js'
import { createConnection } from '../../utils/SignalingStandaloneTest.js'
@@ -237,6 +238,7 @@ export default {
{ caption: t('spreed', 'WebSocket URL'), description: signalingTest.url },
{ caption: t('spreed', 'Available features'), description: signalingTest.features.join(', ') },
]
+ EventBus.emit('signaling-server-connected', signalingTest)
} catch (exception) {
console.error(exception)
this.errorMessage = t('spreed', 'Error: Websocket connection failed. Check browser console')
diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts
index 44d9a7f79c1..5e26b7f2907 100644
--- a/src/types/openapi/openapi-administration.ts
+++ b/src/types/openapi/openapi-administration.ts
@@ -232,6 +232,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-backend-recording.ts b/src/types/openapi/openapi-backend-recording.ts
index 5a9ac82cbd9..7408cf07951 100644
--- a/src/types/openapi/openapi-backend-recording.ts
+++ b/src/types/openapi/openapi-backend-recording.ts
@@ -66,6 +66,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-backend-signaling.ts b/src/types/openapi/openapi-backend-signaling.ts
index 6630167c454..466b1efebdc 100644
--- a/src/types/openapi/openapi-backend-signaling.ts
+++ b/src/types/openapi/openapi-backend-signaling.ts
@@ -52,6 +52,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts
index 3d136a1bec7..ce2b5875e90 100644
--- a/src/types/openapi/openapi-backend-sipbridge.ts
+++ b/src/types/openapi/openapi-backend-sipbridge.ts
@@ -147,6 +147,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-bots.ts b/src/types/openapi/openapi-bots.ts
index 35db1d924d6..bc8af5259b7 100644
--- a/src/types/openapi/openapi-bots.ts
+++ b/src/types/openapi/openapi-bots.ts
@@ -70,6 +70,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts
index c187b88d222..abb288bd4cd 100644
--- a/src/types/openapi/openapi-federation.ts
+++ b/src/types/openapi/openapi-federation.ts
@@ -178,6 +178,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts
index 4bd99ce9082..2185d2998b9 100644
--- a/src/types/openapi/openapi-full.ts
+++ b/src/types/openapi/openapi-full.ts
@@ -2015,6 +2015,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts
index 08a7108ac0b..0cd42ea62b7 100644
--- a/src/types/openapi/openapi.ts
+++ b/src/types/openapi/openapi.ts
@@ -1515,6 +1515,7 @@ export type components = {
/** Format: int64 */
"max-duration": number;
"blur-virtual-background": boolean;
+ "end-to-end-encryption": boolean;
};
chat: {
/** Format: int64 */
diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php
index 3658a81c300..931c15775d4 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -136,6 +136,7 @@ public function testGetCapabilitiesGuest(): void {
'start-without-media' => false,
'max-duration' => 0,
'blur-virtual-background' => false,
+ 'end-to-end-encryption' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',
@@ -275,6 +276,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea
'start-without-media' => false,
'max-duration' => 0,
'blur-virtual-background' => false,
+ 'end-to-end-encryption' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',
diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php
index 1523388e0ba..e63791c1116 100644
--- a/tests/php/Controller/SignalingControllerTest.php
+++ b/tests/php/Controller/SignalingControllerTest.php
@@ -20,7 +20,6 @@
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\BanService;
-use OCA\Talk\Service\CertificateService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomService;
use OCA\Talk\Service\SessionService;
@@ -65,7 +64,6 @@ class SignalingControllerTest extends TestCase {
protected TalkSession&MockObject $session;
protected \OCA\Talk\Signaling\Manager&MockObject $signalingManager;
protected Manager|MockObject $manager;
- protected CertificateService&MockObject $certificateService;
protected ParticipantService&MockObject $participantService;
protected SessionService&MockObject $sessionService;
protected Messages&MockObject $messages;
@@ -108,7 +106,6 @@ public function setUp(): void {
$this->dbConnection = \OCP\Server::get(IDBConnection::class);
$this->signalingManager = $this->createMock(\OCA\Talk\Signaling\Manager::class);
$this->manager = $this->createMock(Manager::class);
- $this->certificateService = $this->createMock(CertificateService::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->sessionService = $this->createMock(SessionService::class);
$this->messages = $this->createMock(Messages::class);
@@ -128,7 +125,6 @@ private function recreateSignalingController() {
$this->signalingManager,
$this->session,
$this->manager,
- $this->certificateService,
$this->participantService,
$this->sessionService,
$this->dbConnection,