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 @@