diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2022-08-12 16:07:52 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-12 16:07:52 +0200 |
commit | 91d3bd8df8a5288c2abb3badde00c248d974dae8 (patch) | |
tree | c90b1ec7d0600f7119959532167c5fbd1f2b0b8f /lib | |
parent | b6c2ad7feef446b992d087d84f0c5101a6ef5000 (diff) | |
parent | ad8134738219349269fc4d7191b2a1a205178ca9 (diff) |
Merge pull request #7472 from nextcloud/jwt-auth
Implement JWT auth for signaling connections (hello v2)
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Capabilities.php | 5 | ||||
-rw-r--r-- | lib/Config.php | 124 | ||||
-rw-r--r-- | lib/Controller/SignalingController.php | 20 | ||||
-rw-r--r-- | lib/Signaling/Manager.php | 3 |
4 files changed, 146 insertions, 6 deletions
diff --git a/lib/Capabilities.php b/lib/Capabilities.php index f0a84e74e..c388347be 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -150,6 +150,11 @@ class Capabilities implements IPublicCapability { $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 1628aa1cd..b3dda8d85 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -23,12 +23,16 @@ declare(strict_types=1); 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,10 +353,26 @@ class Config { } /** + * @param int $version * @param string $userId * @return string */ - public function getSignalingTicket(?string $userId): string { + 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 { @@ -369,6 +398,99 @@ class Config { 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) === 'ES') { + $privKey = openssl_pkey_new([ + 'curve_name' => 'prime256v1', + 'private_key_type' => OPENSSL_KEYTYPE_EC, + ]); + $pubKey = openssl_pkey_get_details($privKey); + $public = $pubKey['key']; + if (!openssl_pkey_export($privKey, $secret)) { + throw new \Exception('Could not export private key'); + } + } elseif (substr($alg, 0, 2) === 'RS') { + $privKey = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + $pubKey = openssl_pkey_get_details($privKey); + $public = $pubKey['key']; + if (!openssl_pkey_export($privKey, $secret)) { + throw new \Exception('Could not export private key'); + } + } elseif ($alg === 'EdDSA') { + $privKey = sodium_crypto_sign_keypair(); + $public = base64_encode(sodium_crypto_sign_publickey($privKey)); + $secret = base64_encode(sodium_crypto_sign_secretkey($privKey)); + } else { + throw new \Exception('Unsupported algorithm ' . $alg); + } + + $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 40a2ac3e5..20954020f 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -46,6 +46,7 @@ use OCP\AppFramework\Utility\ITimeFactory; 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 @@ class SignalingController extends OCSController { LoggerInterface $logger, ?string $UserId) { parent::__construct($appName, $request); + $this->serverConfig = $serverConfig; $this->talkConfig = $talkConfig; $this->signalingManager = $signalingManager; $this->session = $session; @@ -160,12 +164,22 @@ class SignalingController extends OCSController { $signalingMode = $this->talkConfig->getSignalingMode(); $signaling = $this->signalingManager->getSignalingServerLinkForConversation($room); + $helloAuthParams = [ + '1.0' => [ + 'userid' => $this->userId, + 'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId), + ], + '2.0' => [ + 'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId), + ], + ]; $data = [ 'signalingMode' => $signalingMode, 'userId' => $this->userId, 'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(), 'server' => $signaling, - 'ticket' => $this->talkConfig->getSignalingTicket($this->userId), + 'ticket' => $helloAuthParams['1.0']['ticket'], + 'helloAuthParams' => $helloAuthParams, 'stunservers' => $stun, 'turnservers' => $turn, 'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '', @@ -546,9 +560,7 @@ class SignalingController extends OCSController { ]; 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/lib/Signaling/Manager.php b/lib/Signaling/Manager.php index b69561343..87f472900 100644 --- a/lib/Signaling/Manager.php +++ b/lib/Signaling/Manager.php @@ -54,7 +54,8 @@ class Manager { $features = explode(',', $featureHeader); $features = array_map('trim', $features); return in_array('audio-video-permissions', $features, true) - && in_array('incall-all', $features, true); + && in_array('incall-all', $features, true) + && in_array('hello-v2', $features, true); } public function getSignalingServerLinkForConversation(?Room $room): string { |