Passed
Push — master ( dd2d9f...8460b0 )
by Tim
13:58
created

OpenSSL::encrypt()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 27
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 40
rs 8.8657
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\Backend;
6
7
use SimpleSAML\XMLSecurity\Alg\KeyTransport;
8
use SimpleSAML\XMLSecurity\Constants as C;
9
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
10
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
11
use SimpleSAML\XMLSecurity\Key\KeyInterface;
12
use SimpleSAML\XMLSecurity\Key\AsymmetricKey;
13
use SimpleSAML\XMLSecurity\Key\PrivateKey;
14
use SimpleSAML\XMLSecurity\Utils\Random;
15
16
use function chr;
17
use function mb_strlen;
18
use function openssl_cipher_iv_length;
19
use function openssl_decrypt;
20
use function openssl_encrypt;
21
use function openssl_error_string;
22
use function openssl_private_decrypt;
23
use function openssl_public_decrypt;
24
use function openssl_private_encrypt;
25
use function openssl_public_encrypt;
26
use function openssl_sign;
27
use function openssl_verify;
28
use function ord;
29
use function str_repeat;
30
use function strval;
31
use function substr;
32
33
/**
34
 * Backend for encryption and digital signatures based on the native openssl library.
35
 *
36
 * @package SimpleSAML\XMLSecurity\Backend
37
 */
38
final class OpenSSL implements EncryptionBackend, SignatureBackend
39
{
40
    // digital signature options
41
    /** @var string */
42
    protected string $digest;
43
44
    // asymmetric encryption options
45
    /** @var int */
46
    protected int $padding = OPENSSL_PKCS1_OAEP_PADDING;
47
48
    // symmetric encryption options
49
    /** @var string */
50
    protected string $cipher;
51
52
    /** @var int */
53
    protected int $blocksize;
54
55
    /** @var int */
56
    protected int $keysize;
57
58
    /** @var bool */
59
    protected bool $useAuthTag = false;
60
61
    /** @var int */
62
    public const AUTH_TAG_LEN = 16;
63
64
65
    /**
66
     * Build a new OpenSSL backend.
67
     */
68
    public function __construct()
69
    {
70
        $this->setDigestAlg(C::DIGEST_SHA256);
71
        $this->setCipher(C::BLOCK_ENC_AES128_GCM);
72
    }
73
74
75
    /**
76
     * Encrypt a given plaintext with this cipher and a given key.
77
     *
78
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to encrypt.
79
     * @param string $plaintext The original text to encrypt.
80
     *
81
     * @return string The encrypted plaintext (ciphertext).
82
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is an error while encrypting the plaintext.
83
     */
84
    public function encrypt(KeyInterface $key, string $plaintext): string
85
    {
86
        if ($key instanceof AsymmetricKey) {
87
            // asymmetric encryption
88
            $fn = 'openssl_public_encrypt';
89
            if ($key instanceof PrivateKey) {
90
                $fn = 'openssl_private_encrypt';
91
            }
92
93
            $ciphertext = '';
94
            if (!$fn($plaintext, $ciphertext, $key->getMaterial(), $this->padding)) {
95
                throw new RuntimeException('Cannot encrypt data: ' . openssl_error_string());
96
            }
97
            return $ciphertext;
98
        }
99
100
        // symmetric encryption
101
        $ivlen = openssl_cipher_iv_length($this->cipher);
102
        $iv = Random::generateRandomBytes($ivlen);
103
        $data = $this->pad($plaintext);
104
        $authTag = null;
105
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
106
        if ($this->useAuthTag) { // configure GCM mode
107
            $authTag = Random::generateRandomBytes(self::AUTH_TAG_LEN);
108
            $options = OPENSSL_RAW_DATA;
109
            $data = $plaintext;
110
        }
111
        $ciphertext = openssl_encrypt(
112
            $data,
113
            $this->cipher,
114
            $key->getMaterial(),
115
            $options,
116
            $iv,
117
            $authTag,
118
        );
119
120
        if (!$ciphertext) {
121
            throw new RuntimeException('Cannot encrypt data: ' . openssl_error_string());
122
        }
123
        return $iv . $ciphertext . $authTag;
124
    }
125
126
127
    /**
128
     * Decrypt a given ciphertext with this cipher and a given key.
129
     *
130
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to decrypt.
131
     * @param string $ciphertext The encrypted text to decrypt.
132
     *
133
     * @return string The decrypted ciphertext (plaintext).
134
     *
135
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is an error while decrypting the ciphertext.
136
     */
137
    public function decrypt(KeyInterface $key, string $ciphertext): string
138
    {
139
        if ($key instanceof AsymmetricKey) {
140
            // asymmetric encryption
141
            $fn = 'openssl_public_decrypt';
142
            if ($key instanceof PrivateKey) {
143
                $fn = 'openssl_private_decrypt';
144
            }
145
146
            $plaintext = '';
147
            if (!$fn($ciphertext, $plaintext, $key->getMaterial(), $this->padding)) {
148
                throw new RuntimeException('Cannot decrypt data: ' . openssl_error_string());
149
            }
150
            return $plaintext;
151
        }
152
153
        // symmetric encryption
154
        $ivlen = openssl_cipher_iv_length($this->cipher);
155
        $iv = substr($ciphertext, 0, $ivlen);
156
        $ciphertext = substr($ciphertext, $ivlen);
157
158
        $authTag = null;
159
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
160
        if ($this->useAuthTag) { // configure GCM mode
161
            $authTag = substr($ciphertext, - self::AUTH_TAG_LEN);
162
            $ciphertext = substr($ciphertext, 0, - self::AUTH_TAG_LEN);
163
            $options = OPENSSL_RAW_DATA;
164
        }
165
166
        $plaintext = openssl_decrypt(
167
            $ciphertext,
168
            $this->cipher,
169
            $key->getMaterial(),
170
            $options,
171
            $iv,
172
            strval($authTag), /** @TODO remove strval when minimal php-version becomes >=8.1 */
173
        );
174
175
        if ($plaintext === false) {
176
            throw new RuntimeException('Cannot decrypt data: ' . openssl_error_string());
177
        }
178
        return $this->useAuthTag ? $plaintext : $this->unpad($plaintext);
179
    }
180
181
182
    /**
183
     * Sign a given plaintext with this cipher and a given key.
184
     *
185
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to sign.
186
     * @param string $plaintext The original text to sign.
187
     *
188
     * @return string The (binary) signature corresponding to the given plaintext.
189
     *
190
     * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException If there is an error while signing the plaintext.
191
     */
192
    public function sign(KeyInterface $key, string $plaintext): string
193
    {
194
        if (!openssl_sign($plaintext, $signature, $key->getMaterial(), $this->digest)) {
195
            throw new RuntimeException('Cannot sign data: ' . openssl_error_string());
196
        }
197
        return $signature;
198
    }
199
200
201
    /**
202
     * Verify a signature with this cipher and a given key.
203
     *
204
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to verify.
205
     * @param string $plaintext The original signed text.
206
     * @param string $signature The (binary) signature to verify.
207
     *
208
     * @return boolean True if the signature can be verified, false otherwise.
209
     */
210
    public function verify(KeyInterface $key, string $plaintext, string $signature): bool
211
    {
212
        return openssl_verify($plaintext, $signature, $key->getMaterial(), $this->digest) === 1;
213
    }
214
215
216
    /**
217
     * Set the cipher to be used by the backend.
218
     *
219
     * @param \SimpleSAML\XMLSecurity\Alg\KeyTransport|string $cipher The identifier of the cipher.
220
     *
221
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the cipher is unknown or not supported.
222
     */
223
    public function setCipher(KeyTransport|string $cipher): void
224
    {
225
        if (is_string($cipher) && !isset(C::$BLOCK_CIPHER_ALGORITHMS[$cipher]) {
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '{' on line 225 at column 79
Loading history...
226
            throw new InvalidArgumentException('Invalid or unknown cipher');
227
        }
228
229
        // configure the backend depending on the actual algorithm to use
230
        $this->useAuthTag = false;
231
        $this->cipher = $cipher;
232
        switch ($cipher) {
233
            case KeyTransport::RSA_1_5:
234
                $this->padding = OPENSSL_PKCS1_PADDING;
235
                break;
236
            case KeyTransport::OAEP:
237
            case KeyTransport::OAEP_MGF1P:
238
                $this->padding = OPENSSL_PKCS1_OAEP_PADDING;
239
                break;
240
            case C::BLOCK_ENC_AES128_GCM:
241
            case C::BLOCK_ENC_AES192_GCM:
242
            case C::BLOCK_ENC_AES256_GCM:
243
                $this->useAuthTag = true;
244
                // Intentional fall-thru
245
            default:
246
                $this->cipher = C::$BLOCK_CIPHER_ALGORITHMS[$cipher];
247
                $this->blocksize = C::$BLOCK_SIZES[$cipher];
248
                $this->keysize = C::$BLOCK_CIPHER_KEY_SIZES[$cipher];
249
        }
250
    }
251
252
253
    /**
254
     * Set the digest algorithm to be used by this backend.
255
     *
256
     * @param string $digest The identifier of the digest algorithm.
257
     *
258
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the given digest is not valid.
259
     */
260
    public function setDigestAlg(string $digest): void
261
    {
262
        if (!isset(C::$DIGEST_ALGORITHMS[$digest])) {
263
            throw new InvalidArgumentException('Unknown digest or non-cryptographic hash function.');
264
        }
265
        $this->digest = C::$DIGEST_ALGORITHMS[$digest];
266
    }
267
268
269
    /**
270
     * Pad a plaintext using ISO 10126 padding.
271
     *
272
     * @param string $plaintext The plaintext to pad.
273
     *
274
     * @return string The padded plaintext.
275
     */
276
    public function pad(string $plaintext): string
277
    {
278
        $padchr = $this->blocksize - (mb_strlen($plaintext) % $this->blocksize);
279
        $pattern = chr($padchr);
280
        return $plaintext . str_repeat($pattern, $padchr);
281
    }
282
283
284
    /**
285
     * Remove an existing ISO 10126 padding from a given plaintext.
286
     *
287
     * @param string $plaintext The padded plaintext.
288
     *
289
     * @return string The plaintext without the padding.
290
     */
291
    public function unpad(string $plaintext): string
292
    {
293
        return substr($plaintext, 0, -ord(substr($plaintext, -1)));
294
    }
295
}
296