Passed
Branch feature/php83 (e52173)
by Tim
33:00 queued 16:50
created

OpenSSL::encrypt()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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