diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 3fd9d829a..e4b439786 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Authentication\Exceptions\InvalidTokenException; @@ -60,8 +61,9 @@ public function __construct( * 401: Missing permissions to register device */ #[NoAdminRequired] + #[UserRateLimit(limit: 10, period: 3600)] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/push', requirements: ['apiVersion' => '(v2)'])] - public function registerDevice(string $pushTokenHash, string $devicePublicKey, string $proxyServer): DataResponse { + public function registerDevice(#[\SensitiveParameter] string $pushTokenHash, string $devicePublicKey, string $proxyServer): DataResponse { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { return new DataResponse([], Http::STATUS_UNAUTHORIZED); @@ -179,7 +181,7 @@ public function removeDevice(): DataResponse { /** * @return bool If the hash was new to the database */ - protected function savePushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool { + protected function savePushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, #[\SensitiveParameter] string $pushTokenHash, string $proxyServer, string $appType): bool { $query = $this->db->getQueryBuilder(); $query->select('*') ->from('notifications_pushhash') @@ -202,7 +204,7 @@ protected function savePushToken(IUser $user, IToken $token, string $deviceIdent /** * @return bool If the entry was created */ - protected function insertPushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool { + protected function insertPushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, #[\SensitiveParameter] string $pushTokenHash, string $proxyServer, string $appType): bool { $devicePublicKeyHash = hash('sha512', $devicePublicKey); $query = $this->db->getQueryBuilder(); @@ -223,7 +225,7 @@ protected function insertPushToken(IUser $user, IToken $token, string $deviceIde /** * @return bool If the entry was updated */ - protected function updatePushToken(IUser $user, IToken $token, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool { + protected function updatePushToken(IUser $user, IToken $token, string $devicePublicKey, #[\SensitiveParameter] string $pushTokenHash, string $proxyServer, string $appType): bool { $devicePublicKeyHash = hash('sha512', $devicePublicKey); $query = $this->db->getQueryBuilder(); @@ -254,7 +256,7 @@ protected function deletePushToken(IUser $user, IToken $token): bool { /** * @return bool If the entry was deleted */ - protected function deletePushTokenByHash(IUser $user, string $pushTokenHash): bool { + protected function deletePushTokenByHash(IUser $user, #[\SensitiveParameter] string $pushTokenHash): bool { $query = $this->db->getQueryBuilder(); $query->delete('notifications_pushhash') ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) diff --git a/lib/Controller/WebPushController.php b/lib/Controller/WebPushController.php index 20e209d8f..1c6219ec8 100644 --- a/lib/Controller/WebPushController.php +++ b/lib/Controller/WebPushController.php @@ -18,6 +18,7 @@ use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\AppFramework\Services\IAppConfig; @@ -80,8 +81,9 @@ public function getVapid(): DataResponse { * 403: Web push is disabled by the administrator */ #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 600)] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/webpush', requirements: ['apiVersion' => '(v2)'])] - public function registerWP(string $endpoint, string $uaPublicKey, string $auth, string $appTypes): DataResponse { + public function registerWP(#[\SensitiveParameter] string $endpoint, string $uaPublicKey, #[\SensitiveParameter] string $auth, string $appTypes): DataResponse { if (!$this->appConfig->getAppValueBool('webpush_enabled')) { return new DataResponse(['message' => 'WEBPUSH_DISABLED'], Http::STATUS_FORBIDDEN); } @@ -161,7 +163,7 @@ public function registerWP(string $endpoint, string $uaPublicKey, string $auth, */ #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/webpush/activate', requirements: ['apiVersion' => '(v2)'])] - public function activateWP(string $activationToken): DataResponse { + public function activateWP(#[\SensitiveParameter] string $activationToken): DataResponse { if (!$this->appConfig->getAppValueBool('webpush_enabled')) { return new DataResponse(['message' => 'WEBPUSH_DISABLED'], Http::STATUS_FORBIDDEN); } @@ -244,7 +246,7 @@ protected function getWPClient(): WebPushClient { * - CREATED if the user didn't have an activated subscription with this endpoint, pubkey and auth * - UPDATED if the subscription has been updated (use to change appTypes) */ - protected function saveSubscription(IUser $user, IToken|ISession $token, string $endpoint, string $uaPublicKey, string $auth, string $appTypes): array { + protected function saveSubscription(IUser $user, IToken|ISession $token, #[\SensitiveParameter] string $endpoint, string $uaPublicKey, #[\SensitiveParameter] string $auth, string $appTypes): array { $query = $this->db->getQueryBuilder(); $query->select('*') ->from('notifications_webpush') @@ -282,7 +284,7 @@ protected function saveSubscription(IUser $user, IToken|ISession $token, string * - NO_TOKEN if we don't have this token * - NO_SUB if we don't have this subscription */ - protected function activateSubscription(IUser $user, IToken|ISession $token, string $activationToken): ActivationSubStatus { + protected function activateSubscription(IUser $user, IToken|ISession $token, #[\SensitiveParameter] string $activationToken): ActivationSubStatus { $query = $this->db->getQueryBuilder(); $query->select('*') ->from('notifications_webpush') @@ -316,7 +318,7 @@ protected function activateSubscription(IUser $user, IToken|ISession $token, str * @param string $appTypes comma separated list of types * @return bool If the entry was created */ - protected function insertSubscription(IUser $user, IToken|ISession $token, string $endpoint, string $uaPublicKey, string $auth, string $activationToken, string $appTypes): bool { + protected function insertSubscription(IUser $user, IToken|ISession $token, #[\SensitiveParameter] string $endpoint, string $uaPublicKey, #[\SensitiveParameter] string $auth, #[\SensitiveParameter] string $activationToken, string $appTypes): bool { $query = $this->db->getQueryBuilder(); $query->insert('notifications_webpush') ->values([ @@ -335,7 +337,7 @@ protected function insertSubscription(IUser $user, IToken|ISession $token, strin * @param string $appTypes comma separated list of types * @return bool If the entry was updated */ - protected function updateSubscription(IUser $user, IToken|ISession $token, string $endpoint, string $uaPublicKey, string $auth, string $appTypes): bool { + protected function updateSubscription(IUser $user, IToken|ISession $token, #[\SensitiveParameter] string $endpoint, string $uaPublicKey, #[\SensitiveParameter] string $auth, string $appTypes): bool { $query = $this->db->getQueryBuilder(); $query->update('notifications_webpush') ->set('endpoint', $query->createNamedParameter($endpoint)) diff --git a/lib/Push.php b/lib/Push.php index b0d33ef4f..3b969a455 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -476,6 +476,8 @@ public function proxyPushToDevice(int $id, IUser $user, array $devices, INotific } catch (\InvalidArgumentException) { // Failed to encrypt message for device: public key is invalid $this->deleteProxyPushToken($device['token']); + } catch (\RuntimeException) { + // Failed to sign message, already logged } } $this->printInfo(''); @@ -666,6 +668,8 @@ public function proxyPushDeleteToDevice(string $userId, IUser $user, array $devi } catch (\InvalidArgumentException) { // Failed to encrypt message for device: public key is invalid $this->deleteProxyPushToken($device['token']); + } catch (\RuntimeException) { + // Failed to sign message, already logged } } @@ -975,6 +979,19 @@ protected function getNotifTopicAndUrgency(string $app, string $type): array { return $res; } + /** + * @throws \RuntimeException when the data could not be signed + */ + protected function sign(string $data, #[\SensitiveParameter] string $userPrivateKey): string { + if (!openssl_sign($data, $signature, $userPrivateKey, OPENSSL_ALGO_SHA512)) { + $error = openssl_error_string() ?: 'Unknown OpenSSL error'; + $this->log->error($error, ['app' => 'notifications']); + $this->printInfo('Failed to sign encrypted push subject: "' . $error . '"'); + throw new \RuntimeException('Failed to sign message for device'); + } + return $signature; + } + /** * @param string $userPrivateKey * @param array $device @@ -984,8 +1001,9 @@ protected function getNotifTopicAndUrgency(string $app, string $type): array { * @psalm-return array{deviceIdentifier: string, pushTokenHash: string, subject: string, signature: string, priority: string, type: string} * @throws InvalidTokenException * @throws \InvalidArgumentException + * @throws \RuntimeException */ - protected function encryptAndSign(string $userPrivateKey, array $device, int $id, INotification $notification): array { + protected function encryptAndSign(#[\SensitiveParameter] string $userPrivateKey, #[\SensitiveParameter] array $device, int $id, INotification $notification): array { $data = $this->encodeNotif($id, $notification, 200); $ret = $this->getNotifTopicAndUrgency($data['app'], $data['type']); $priority = $ret['urgency']; @@ -1004,11 +1022,8 @@ protected function encryptAndSign(string $userPrivateKey, array $device, int $id throw new \InvalidArgumentException('Failed to encrypt message for device'); } - if (openssl_sign($encryptedSubject, $signature, $userPrivateKey, OPENSSL_ALGO_SHA512)) { - $this->printInfo('Signed encrypted push subject'); - } else { - $this->printInfo('Failed to signed encrypted push subject'); - } + $signature = $this->sign($encryptedSubject, $userPrivateKey); + $this->printInfo('Signed encrypted push subject'); $base64EncryptedSubject = base64_encode($encryptedSubject); $base64Signature = base64_encode($signature); @@ -1030,8 +1045,9 @@ protected function encryptAndSign(string $userPrivateKey, array $device, int $id * @psalm-return array{remaining: array, payload: array{deviceIdentifier: string, pushTokenHash: string, subject: string, signature: string, priority: string, type: string}} * @throws InvalidTokenException * @throws \InvalidArgumentException + * @throws \RuntimeException */ - protected function encryptAndSignDelete(string $userPrivateKey, array $device, ?array $ids): array { + protected function encryptAndSignDelete(#[\SensitiveParameter] string $userPrivateKey, #[\SensitiveParameter] array $device, ?array $ids): array { $ret = $this->encodeDeleteNotifs($ids); $remainingIds = $ret['remaining']; $data = $ret['data']; @@ -1043,7 +1059,7 @@ protected function encryptAndSignDelete(string $userPrivateKey, array $device, ? throw new \InvalidArgumentException('Failed to encrypt message for device'); } - openssl_sign($encryptedSubject, $signature, $userPrivateKey, OPENSSL_ALGO_SHA512); + $signature = $this->sign($encryptedSubject, $userPrivateKey); $base64EncryptedSubject = base64_encode($encryptedSubject); $base64Signature = base64_encode($signature); @@ -1174,7 +1190,7 @@ protected function deleteWebPushToken(int $tokenId): bool { * @param string $endpoint * @return bool */ - protected function deleteWebPushTokenByEndpoint(string $endpoint): bool { + protected function deleteWebPushTokenByEndpoint(#[\SensitiveParameter] string $endpoint): bool { $query = $this->db->getQueryBuilder(); $query->delete('notifications_webpush') ->where($query->expr()->eq('endpoint', $query->createNamedParameter($endpoint))); diff --git a/lib/WebPushClient.php b/lib/WebPushClient.php index a0774592d..9c67d45c0 100644 --- a/lib/WebPushClient.php +++ b/lib/WebPushClient.php @@ -39,7 +39,7 @@ public static function isValidP256dh(string $key): bool { return true; } - public static function isValidAuth(string $auth): bool { + public static function isValidAuth(#[\SensitiveParameter] string $auth): bool { if (!preg_match('/^[A-Za-z0-9_-]{22}=*$/', $auth)) { return false; } @@ -106,7 +106,7 @@ public function getVapidPublicKey(): string { /** * Send one notification - blocking (should be avoided most of the time) */ - public function notify(string $endpoint, string $uaPublicKey, string $auth, string $body): void { + public function notify(#[\SensitiveParameter] string $endpoint, string $uaPublicKey, #[\SensitiveParameter] string $auth, #[\SensitiveParameter] string $body): void { $c = $this->getClient(); $c->queueNotification( new Subscription($endpoint, $uaPublicKey, $auth, 'aes128gcm'), diff --git a/src/Components/ActionButton.vue b/src/Components/ActionButton.vue index e7da1a0c3..1ba4f09ba 100644 --- a/src/Components/ActionButton.vue +++ b/src/Components/ActionButton.vue @@ -6,6 +6,7 @@