Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add admin toggle for e2ee calls #14098

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ Legend:
| `enable_matterbridge` | string<br>`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured |
| `force_passwords` | string<br>`1` or `0` | `0` | No | ️ | Whether public chats are forced to use a password |
| `create_samples` | string<br>`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<br>`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<br>`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. |
6 changes: 6 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Capabilities implements IPublicCapability {
'message-expiration',
'reactions',
'chat-summary-api',
'call-end-to-end-encryption',
];

public const LOCAL_FEATURES = [
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
];
Expand Down
9 changes: 9 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
88 changes: 4 additions & 84 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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']);
}

/**
Expand Down
10 changes: 8 additions & 2 deletions lib/Middleware/CanUseTalkMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 55 additions & 6 deletions lib/SetupCheck/HighPerformanceBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@ public function __construct(
readonly protected ICacheFactory $cacheFactory,
readonly protected IURLGenerator $urlGenerator,
readonly protected IL10N $l,
readonly protected Manager $signalManager,
) {
}

Expand Down Expand Up @@ -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();
}
}
Loading
Loading