Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
30 / 33
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationDeliveryKeyFactory
90.91% covered (success)
90.91%
30 / 33
33.33% covered (danger)
33.33%
1 / 3
10.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 correlationId
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 keys
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Notification;
10
11use OCP\IConfig;
12
13/** Derive correlation and idempotency keys without recording recipient data in plaintext. */
14final class NotificationDeliveryKeyFactory {
15    public function __construct(
16        private IConfig $config,
17    ) {
18    }
19
20    /** One opaque ID shared by all channels for this business event. */
21    public function correlationId(string $sourceEventId, string $eventType, int $fileId, ?int $signRequestId): string {
22        if ($sourceEventId === '') {
23            throw new \InvalidArgumentException('Notification event identity is required');
24        }
25        $secret = $this->config->getSystemValueString('secret');
26        if ($secret === '') {
27            throw new \LogicException('The Nextcloud instance secret is not available');
28        }
29        $event = implode("\0", [$sourceEventId, $eventType, (string)$fileId, (string)$signRequestId]);
30        return hash_hmac('sha256', $event, hash_hmac('sha256', 'libresign-notification-event-v1', $secret, true));
31    }
32
33    /** @return array{correlationId: string, eventKey: string} */
34    public function keys(NotificationMessage $message, NotificationChannel $transport): array {
35        if ($message->sourceEventId === '' || trim($message->recipient) === '' || !$transport->supports($message->channel)) {
36            throw new \InvalidArgumentException('Notification event, channel or recipient is invalid');
37        }
38        $secret = $this->config->getSystemValueString('secret');
39        if ($secret === '') {
40            throw new \LogicException('The Nextcloud instance secret is not available');
41        }
42        $recipient = trim($message->recipient);
43        if ($message->channel === 'mail') {
44            $recipient = strtolower($recipient);
45        }
46        $keyed = static fn (string $context, string $payload): string => hash_hmac(
47            'sha256', $payload,
48            hash_hmac('sha256', $context, $secret, true),
49        );
50        return [
51            'correlationId' => $this->correlationId(
52                $message->sourceEventId, $message->eventType, $message->file->getId(), $message->signRequest->getId(),
53            ),
54            'eventKey' => $keyed('libresign-notification-v2', implode("\0", [
55                $message->sourceEventId,
56                $message->eventType,
57                $message->channel,
58                (string)$message->file->getId(),
59                (string)$message->signRequest->getId(),
60                $recipient,
61            ])),
62        ];
63    }
64}