From f38205ae3b5343384d80b9a72e88fa79ba251922 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Fri, 10 Jun 2022 09:33:14 +0200 Subject: [PATCH] Implement JWT auth for signaling connection. Signed-off-by: Joachim Bauch --- lib/Capabilities.php | 5 + lib/Config.php | 118 +++++++++++++++++- lib/Controller/SignalingController.php | 24 +++- src/utils/signaling.js | 22 ++-- src/utils/webrtc/index.js | 6 + .../Controller/SignalingControllerTest.php | 8 +- 6 files changed, 164 insertions(+), 19 deletions(-) diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 57650a54d6a..c6dce161e8c 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -140,6 +140,11 @@ public function getCapabilities(): array { $capabilities['config']['conversations']['can-create'] = $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user); + $pubKey = $this->talkConfig->getSignalingTokenPublicKey(); + if ($pubKey) { + $capabilities['config']['signaling']['hello-v2-token-key'] = $pubKey; + } + if ($this->serverConfig->getAppValue('spreed', 'has_reference_id', 'no') === 'yes') { $capabilities['features'][] = 'chat-reference-id'; } diff --git a/lib/Config.php b/lib/Config.php index 1628aa1cdbb..ae28922f741 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -23,12 +23,16 @@ namespace OCA\Talk; +use Firebase\JWT\JWT; + use OCP\AppFramework\Utility\ITimeFactory; use OCA\Talk\Events\GetTurnServersEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IGroupManager; +use OCP\IURLGenerator; use OCP\IUser; +use OCP\IUserManager; use OCP\Security\ISecureRandom; class Config { @@ -36,9 +40,14 @@ class Config { public const SIGNALING_EXTERNAL = 'external'; public const SIGNALING_CLUSTER_CONVERSATION = 'conversation_cluster'; + public const SIGNALING_TICKET_V1 = 1; + public const SIGNALING_TICKET_V2 = 2; + protected IConfig $config; protected ITimeFactory $timeFactory; private IGroupManager $groupManager; + private IUserManager $userManager; + private IURLGenerator $urlGenerator; private ISecureRandom $secureRandom; private IEventDispatcher $dispatcher; @@ -47,11 +56,15 @@ class Config { public function __construct(IConfig $config, ISecureRandom $secureRandom, IGroupManager $groupManager, + IUserManager $userManager, + IURLGenerator $urlGenerator, ITimeFactory $timeFactory, IEventDispatcher $dispatcher) { $this->config = $config; $this->secureRandom = $secureRandom; $this->groupManager = $groupManager; + $this->userManager = $userManager; + $this->urlGenerator = $urlGenerator; $this->timeFactory = $timeFactory; $this->dispatcher = $dispatcher; } @@ -340,11 +353,27 @@ public function getHideSignalingWarning(): bool { } /** + * @param int $version * @param string $userId * @return string */ - public function getSignalingTicket(?string $userId): string { - if (empty($userId)) { + public function getSignalingTicket(int $version, ?string $userId): string { + switch ($version) { + case self::SIGNALING_TICKET_V1: + return $this->getSignalingTicketV1($userId); + case self::SIGNALING_TICKET_V2: + return $this->getSignalingTicketV2($userId); + default: + return $this->getSignalingTicketV1($userId); + } + } + + /** + * @param string $userId + * @return string + */ + private function getSignalingTicketV1(?string $userId): string { + if (empty($userId)) { $secret = $this->config->getAppValue('spreed', 'signaling_ticket_secret'); } else { $secret = $this->config->getUserValue($userId, 'spreed', 'signaling_ticket_secret'); @@ -369,6 +398,91 @@ public function getSignalingTicket(?string $userId): string { return $data . ':' . $hash; } + private function ensureSignalingTokenKeys(string $alg): void { + $secret = $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg)); + if ($secret) { + return; + } + + if (substr($alg, 0, 2) === 'EC') { + $privKey = openssl_pkey_new([ + 'curve_name' => 'prime256v1', + 'private_key_type' => OPENSSL_KEYTYPE_EC, + ]); + } else if (substr($alg, 0, 2) === 'RS') { + $privKey = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + } else { + throw new \Exception('Unsupported algorithm ' . $alg); + } + $pubKey = openssl_pkey_get_details($privKey); + $public = $pubKey['key']; + + if (!openssl_pkey_export($privKey, $secret)) { + throw new \Exception('Could not export private key'); + } + + $this->config->setAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg), $secret); + $this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $public); + } + + public function getSignalingTokenAlgorithm(): string { + return $this->config->getAppValue('spreed', 'signaling_token_alg', 'ES256');; + } + + public function getSignalingTokenPrivateKey(?string $alg = null): string { + if (!$alg) { + $alg = $this->getSignalingTokenAlgorithm(); + } + $this->ensureSignalingTokenKeys($alg); + + return $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg)); + } + + public function getSignalingTokenPublicKey(?string $alg = null): string { + if (!$alg) { + $alg = $this->getSignalingTokenAlgorithm(); + } + $this->ensureSignalingTokenKeys($alg); + + return $this->config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg)); + } + + /** + * @param IUser $user + * @return array + */ + public function getSignalingUserData(IUser $user): array { + return [ + 'displayname' => $user->getDisplayName(), + ]; + } + + /** + * @param string $userId + * @return string + */ + private function getSignalingTicketV2(?string $userId): string { + $timestamp = $this->timeFactory->getTime(); + $data = [ + 'iss' => $this->urlGenerator->getAbsoluteURL(''), + 'iat' => $timestamp, + 'exp' => $timestamp + 60, // Valid for 1 minute. + ]; + $user = !empty($userId) ? $this->userManager->get($userId) : null; + if ($user instanceof IUser) { + $data['sub'] = $user->getUID(); + $data['userdata'] = $this->getSignalingUserData($user); + } + + $alg = $this->getSignalingTokenAlgorithm(); + $secret = $this->getSignalingTokenPrivateKey($alg); + $token = JWT::encode($data, $secret, $alg); + return $token; + } + /** * @param string $userId * @param string $ticket diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 4f9bc62ba5c..1ec4fe066e8 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -46,6 +46,7 @@ use OCP\DB\Exception; use OCP\EventDispatcher\IEventDispatcher; use OCP\Http\Client\IClientService; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IRequest; use OCP\IUser; @@ -59,6 +60,7 @@ class SignalingController extends OCSController { public const EVENT_BACKEND_SIGNALING_ROOMS = self::class . '::signalingBackendRoom'; + private IConfig $serverConfig; private Config $talkConfig; private \OCA\Talk\Signaling\Manager $signalingManager; private TalkSession $session; @@ -76,6 +78,7 @@ class SignalingController extends OCSController { public function __construct(string $appName, IRequest $request, + IConfig $serverConfig, Config $talkConfig, \OCA\Talk\Signaling\Manager $signalingManager, TalkSession $session, @@ -91,6 +94,7 @@ public function __construct(string $appName, LoggerInterface $logger, ?string $UserId) { parent::__construct($appName, $request); + $this->serverConfig = $serverConfig; $this->talkConfig = $talkConfig; $this->signalingManager = $signalingManager; $this->session = $session; @@ -157,12 +161,26 @@ public function getSettings(string $token = ''): DataResponse { $signalingMode = $this->talkConfig->getSignalingMode(); $signaling = $this->signalingManager->getSignalingServerLinkForConversation($room); + // TODO: Autodetect and use if signaling server has feature flag "hello-v2". + if ($this->serverConfig->getAppValue('spreed', 'use-hello-v2', false)) { + $helloVersion = '2.0'; + $helloAuthParams = [ + 'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId), + ]; + } else { + $helloVersion = '1.0'; + $helloAuthParams = [ + 'userid' => $this->userId, + 'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId), + ]; + } $data = [ 'signalingMode' => $signalingMode, 'userId' => $this->userId, 'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(), 'server' => $signaling, - 'ticket' => $this->talkConfig->getSignalingTicket($this->userId), + 'helloVersion' => $helloVersion, + 'helloAuthParams' => $helloAuthParams, 'stunservers' => $stun, 'turnservers' => $turn, 'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '', @@ -540,9 +558,7 @@ private function backendAuth(array $auth): DataResponse { ]; if (!empty($userId)) { $response['auth']['userid'] = $user->getUID(); - $response['auth']['user'] = [ - 'displayname' => $user->getDisplayName(), - ]; + $response['auth']['user'] = $this->talkConfig->getSignalingUserData($user); } $this->logger->debug('Validated signaling ticket for {user}', [ 'user' => !empty($userId) ? $userId : '(guests)', diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 6c711961d12..804a4788dbf 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -744,10 +744,17 @@ Signaling.Standalone.prototype.connect = function() { this._trigger('message', [message]) break case 'error': - if (data.error.code === 'processing_failed') { - console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file') - } else { - console.error('Ignore unknown error', data) + switch (data.error.code) { + case 'processing_failed': + console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file') + break + case 'token_expired': + console.info('The signaling token is expired, need to update settings') + this._trigger('updateSettings') + break + default: + console.error('Ignore unknown error', data) + break } break default: @@ -904,13 +911,10 @@ Signaling.Standalone.prototype.sendHello = function() { msg = { type: 'hello', hello: { - version: '1.0', + version: this.settings.helloVersion, auth: { url, - params: { - userid: this.settings.userId, - ticket: this.settings.ticket, - }, + params: this.settings.helloAuthParams, }, }, } diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index 5840e3795f2..2f2db261f21 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -101,6 +101,12 @@ async function connectSignaling(token) { if (!signaling) { signaling = Signaling.createConnection(settings) + signaling.on('updateSettings', async function() { + const settings = await getSignalingSettings(token) + console.debug('Received updated settings', settings) + signaling.settings = settings + }) + } tokensInSignaling[token] = true diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index 1b6329d26cd..a33052d0d26 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -273,7 +273,7 @@ public function testBackendAuth() { 'auth' => [ 'params' => [ 'userid' => 'invalid-userid', - 'ticket' => $this->config->getSignalingTicket($this->userId), + 'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId), ], ], ]); @@ -291,7 +291,7 @@ public function testBackendAuth() { 'auth' => [ 'params' => [ 'userid' => 'unknown-userid', - 'ticket' => $this->config->getSignalingTicket('unknown-userid'), + 'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, 'unknown-userid'), ], ], ]); @@ -320,7 +320,7 @@ public function testBackendAuth() { 'auth' => [ 'params' => [ 'userid' => $this->userId, - 'ticket' => $this->config->getSignalingTicket($this->userId), + 'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId), ], ], ]); @@ -341,7 +341,7 @@ public function testBackendAuth() { 'auth' => [ 'params' => [ 'userid' => '', - 'ticket' => $this->config->getSignalingTicket(''), + 'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, ''), ], ], ]);