Lines 100.00% 37 / 37
Methods 100.00% 3 / 3
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 decrypt 100.00% 25 / 25 100.00% 1 / 1 16
 guard 100.00% 11 / 11 100.00% 1 / 1 4
10final readonly class TatraBankaMailDecryptor
11{
12    public function __construct(
13        private string $privateKeyPath,
14        private string $passphrase,
15    ) {}
16
17    public function decrypt(string $contents): string
18    {
19        if ($this->privateKeyPath === '' || !is_file($this->privateKeyPath)) {
20            throw new ConfigurationException('missing path to TatraBanka PGP private key in config');
21        }
22
23        if (!is_readable($this->privateKeyPath)) {
24            throw new ConfigurationException('failed to read private key file');
25        }
26
27        $fileContents = file_get_contents($this->privateKeyPath);
28        if ($fileContents === false || $fileContents === '') {
29            throw new ConfigurationException('failed to read private key file');
30        }
31
32        return $this->guard(function () use ($fileContents, $contents): string {
33            try {
34                $parsedKey = \OpenPGP_Message::parse($fileContents);
35            } catch (DecryptionException $exception) {
36                throw new ConfigurationException('failed to read private key file', 0, $exception);
37            }
38
39            $secret = null;
40            // @phpstan-ignore foreach.nonIterable (OpenPGP library has dynamic types)
41            foreach ($parsedKey as $packet) {
42                if (!($packet instanceof \OpenPGP_SecretKeyPacket || $packet instanceof \OpenPGP_SecretSubkeyPacket)) {
43                    continue;
44                }
45
46                $secret = \OpenPGP_Crypt_Symmetric::decryptSecretKey($this->passphrase, $packet);
47            }
48
49            if (!($secret instanceof \OpenPGP_SecretKeyPacket || $secret instanceof \OpenPGP_SecretSubkeyPacket)) {
50                throw new ConfigurationException('Private key passphrase is incorrect or the key has no usable secret key');
51            }
52
53            $msg = \OpenPGP_Message::parse(\OpenPGP::unarmor($contents, 'PGP MESSAGE'));
54            $decryptor = new \OpenPGP_Crypt_RSA($secret);
55            $decrypted = $decryptor->decrypt($msg);
56
57            if ($decrypted && isset($decrypted->packets[0]->data) && is_string($decrypted->packets[0]->data) && $decrypted->packets[0]->data !== '') {
58                return $decrypted->packets[0]->data;
59            }
60
61            throw new DecryptionException('PGP message could not be decrypted');
62        });
63    }
64
65    /**
66     * @param callable(): string $callback
67     */
68    private function guard(callable $callback): string
69    {
70        set_error_handler(static function (int $severity, string $message): never {
71            throw new DecryptionException($message);
72        });
73
74        try {
75            return $callback();
76        } catch (ConfigurationException $exception) {
77            throw $exception;
78        } catch (DecryptionException $exception) {
79            throw $exception;
80        } catch (\Throwable $exception) {
81            throw new DecryptionException($exception->getMessage(), 0, $exception);
82        } finally {
83            restore_error_handler();
84        }
85    }
86}