summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2022-08-12 16:07:52 +0200
committerGitHub <noreply@github.com>2022-08-12 16:07:52 +0200
commit91d3bd8df8a5288c2abb3badde00c248d974dae8 (patch)
treec90b1ec7d0600f7119959532167c5fbd1f2b0b8f /lib
parentb6c2ad7feef446b992d087d84f0c5101a6ef5000 (diff)
parentad8134738219349269fc4d7191b2a1a205178ca9 (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.php5
-rw-r--r--lib/Config.php124
-rw-r--r--lib/Controller/SignalingController.php20
-rw-r--r--lib/Signaling/Manager.php3
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 {