Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
AccountCertificateService
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
10 / 10
23
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateCertificateData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 getCertificateEngineName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSetupOk
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uploadPfx
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 deletePfx
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 updatePfxPassword
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 readPfxData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 hasSignatureFile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 createForUser
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
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;
10
11use InvalidArgumentException;
12use OCA\Libresign\Enum\CRLReason;
13use OCA\Libresign\Exception\InvalidPasswordException;
14use OCA\Libresign\Exception\LibresignException;
15use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
16use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
17use OCA\Libresign\Helper\FileUploadHelper;
18use OCA\Libresign\Service\Crl\CrlService;
19use OCP\Files\IMimeTypeDetector;
20use OCP\IL10N;
21use OCP\IUser;
22
23class AccountCertificateService {
24    public function __construct(
25        private IL10N $l10n,
26        private CertificateEngineFactory $certificateEngineFactory,
27        private Pkcs12Handler $pkcs12Handler,
28        private FileUploadHelper $uploadHelper,
29        private IMimeTypeDetector $mimeTypeDetector,
30        private CrlService $crlService,
31    ) {
32    }
33
34    public function validateCertificateData(array $data): void {
35        if (array_key_exists('email', $data['user']) && empty($data['user']['email'])) {
36            // TRANSLATORS Account setup error when the user must have an email address in their Nextcloud profile before continuing.
37            throw new LibresignException($this->l10n->t('You must have an email. You can define the email in your profile.'), 1);
38        }
39        if (!empty($data['user']['email']) && !filter_var($data['user']['email'], FILTER_VALIDATE_EMAIL)) {
40            // TRANSLATORS Account setup error when the email provided to create or update a signing account is invalid.
41            throw new LibresignException($this->l10n->t('Invalid email'), 1);
42        }
43        if (empty($data['signPassword'])) {
44            // TRANSLATORS Account setup error when the dedicated password used to unlock the signing certificate is missing.
45            throw new LibresignException($this->l10n->t('Password to sign is mandatory'), 1);
46        }
47    }
48
49    public function getCertificateEngineName(): string {
50        return $this->certificateEngineFactory->getEngine()->getName();
51    }
52
53    public function isSetupOk(): bool {
54        return $this->certificateEngineFactory->getEngine()->isSetupOk();
55    }
56
57    /**
58     * @throws LibresignException at savePfx
59     * @throws InvalidArgumentException
60     */
61    public function uploadPfx(array $file, IUser $user): void {
62        try {
63            $this->uploadHelper->validateUploadedFile($file);
64        } catch (InvalidArgumentException) {
65            // TRANSLATORS Error when the uploaded certificate file is not valid
66            throw new InvalidArgumentException($this->l10n->t('Invalid file provided. Need to be a .pfx file.'));
67        }
68
69        if ($file['size'] > 10 * 1024) {
70            // TRANSLATORS Error when the certificate file is bigger than normal
71            throw new InvalidArgumentException($this->l10n->t('File is too big'));
72        }
73        $content = file_get_contents($file['tmp_name']);
74        $mimetype = $this->mimeTypeDetector->detectString($content);
75        if ($mimetype !== 'application/octet-stream') {
76            // TRANSLATORS Error when the mimetype of uploaded file is not valid
77            throw new InvalidArgumentException($this->l10n->t('Invalid file provided. Need to be a .pfx file.'));
78        }
79        $extension = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
80        if ($extension !== 'pfx') {
81            // TRANSLATORS Error when the certificate file is not a pfx file
82            throw new InvalidArgumentException($this->l10n->t('Invalid file provided. Need to be a .pfx file.'));
83        }
84        unlink($file['tmp_name']);
85        $this->pkcs12Handler->savePfx($user->getUID(), $content);
86    }
87
88    public function deletePfx(IUser $user): void {
89        $uid = $user->getUID();
90
91        $this->crlService->revokeUserCertificates(
92            $uid,
93            CRLReason::CESSATION_OF_OPERATION,
94            'Certificate deleted by account owner.',
95            $uid,
96        );
97
98        $this->pkcs12Handler->deletePfx($uid);
99    }
100
101    /**
102     * @throws LibresignException when have not a certificate file
103     */
104    public function updatePfxPassword(IUser $user, string $current, string $new): void {
105        try {
106            $pfx = $this->pkcs12Handler->updatePassword($user->getUID(), $current, $new);
107        } catch (InvalidPasswordException) {
108            // TRANSLATORS Authentication error shown when the account credentials used to access LibreSign are invalid.
109            throw new LibresignException($this->l10n->t('Invalid user or password'));
110        }
111    }
112
113    /**
114     * @throws LibresignException when have not a certificate file
115     */
116    public function readPfxData(IUser $user, string $password): array {
117        try {
118            return $this->pkcs12Handler
119                ->setCertificate($this->pkcs12Handler->getPfxOfCurrentSigner($user->getUID()))
120                ->setPassword($password)
121                ->readCertificate();
122        } catch (InvalidPasswordException) {
123            // TRANSLATORS Authentication error shown when the account credentials used to access LibreSign are invalid.
124            throw new LibresignException($this->l10n->t('Invalid user or password'));
125        }
126    }
127
128    public function hasSignatureFile(?IUser $user = null): bool {
129        if (!$user) {
130            return false;
131        }
132        try {
133            $this->pkcs12Handler->getPfxOfCurrentSigner($user->getUID());
134            return true;
135        } catch (LibresignException) {
136            return false;
137        }
138    }
139
140    public function createForUser(IUser $user, string $signPassword): void {
141        $certificate = $this->pkcs12Handler->generateCertificate(
142            [
143                'host' => $user->getPrimaryEMailAddress(),
144                'uid' => 'account:' . $user->getUID(),
145                'name' => $user->getDisplayName()
146            ],
147            $signPassword,
148            $user->getDisplayName()
149        );
150        $this->pkcs12Handler->savePfx($user->getPrimaryEMailAddress(), $certificate);
151    }
152}