OpenSSL::setCipher()   B
last analyzed

Complexity

Conditions 9
Paths 8

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 20
nc 8
nop 1
dl 0
loc 26
rs 8.0555
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;
0 ignored issues
show
introduced by
The function openssl_cipher_key_length was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
    /** @var int */
35
    public const AUTH_TAG_LEN = 16;
36
37
38
    // digital signature options
39
    /** @var string */
40
    protected string $digest;
41
42
    // asymmetric encryption options
43
    /** @var int */
44
    protected int $padding = OPENSSL_PKCS1_OAEP_PADDING;
45
46
    // symmetric encryption options
47
    /** @var string */
48
    protected string $cipher;
49
50
    /** @var int */
51
    protected int $blocksize;
52
53
    /** @var int */
54
    protected int $keysize;
55
56
    /** @var bool */
57
    protected bool $useAuthTag = false;
58
59
60
    /**
61
     * Build a new OpenSSL backend.
62
     */
63
    public function __construct()
64
    {
65
        $this->setDigestAlg(C::DIGEST_SHA256);
66
        $this->setCipher(C::BLOCK_ENC_AES128_GCM);
67
    }
68
69
70
    /**
71
     * Encrypt a given plaintext with this cipher and a given key.
72
     *
73
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to encrypt.
74
     * @param string $plaintext The original text to encrypt.
75
     *
76
     * @return string The encrypted plaintext (ciphertext).
77
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while encrypting the plaintext.
78
     */
79
    public function encrypt(
80
        #[\SensitiveParameter]
81
        KeyInterface $key,
82
        string $plaintext,
83
    ): string {
84
        if ($key instanceof AsymmetricKey) {
85
            // asymmetric encryption
86
            $fn = 'openssl_public_encrypt';
87
            if ($key instanceof PrivateKey) {
88
                $fn = 'openssl_private_encrypt';
89
            }
90
91
            $ciphertext = '';
92
            if (!$fn($plaintext, $ciphertext, $key->getMaterial(), $this->padding)) {
93
                throw new OpenSSLException('Cannot encrypt data');
94
            }
95
            return $ciphertext;
96
        }
97
98
        // symmetric encryption
99
        $ivlen = openssl_cipher_iv_length($this->cipher);
100
        $iv = Random::generateRandomBytes($ivlen);
101
        $data = $this->pad($plaintext);
102
        $authTag = null;
103
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
104
        if ($this->useAuthTag) { // configure GCM mode
105
            $authTag = Random::generateRandomBytes(self::AUTH_TAG_LEN);
106
            $options = OPENSSL_RAW_DATA;
107
            $data = $plaintext;
108
        }
109
        $ciphertext = openssl_encrypt(
110
            $data,
111
            $this->cipher,
112
            $key->getMaterial(),
113
            $options,
114
            $iv,
115
            $authTag,
116
        );
117
118
        if (!$ciphertext) {
119
            throw new OpenSSLException('Cannot encrypt data');
120
        }
121
        return $iv . $ciphertext . $authTag;
122
    }
123
124
125
    /**
126
     * Decrypt a given ciphertext with this cipher and a given key.
127
     *
128
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to decrypt.
129
     * @param string $ciphertext The encrypted text to decrypt.
130
     *
131
     * @return string The decrypted ciphertext (plaintext).
132
     *
133
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while decrypting the ciphertext.
134
     */
135
    public function decrypt(
136
        #[\SensitiveParameter]
137
        KeyInterface $key,
138
        string $ciphertext,
139
    ): string {
140
        if ($key instanceof AsymmetricKey) {
141
            // asymmetric encryption
142
            $fn = 'openssl_public_decrypt';
143
            if ($key instanceof PrivateKey) {
144
                $fn = 'openssl_private_decrypt';
145
            }
146
147
            $plaintext = '';
148
            if (!$fn($ciphertext, $plaintext, $key->getMaterial(), $this->padding)) {
149
                throw new OpenSSLException('Cannot decrypt data');
150
            }
151
            return $plaintext;
152
        }
153
154
        // symmetric encryption
155
        $ivlen = openssl_cipher_iv_length($this->cipher);
156
        $iv = substr($ciphertext, 0, $ivlen);
157
        $ciphertext = substr($ciphertext, $ivlen);
158
159
        $authTag = null;
160
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
161
        if ($this->useAuthTag) { // configure GCM mode
162
            $authTag = substr($ciphertext, - self::AUTH_TAG_LEN);
163
            $ciphertext = substr($ciphertext, 0, - self::AUTH_TAG_LEN);
164
            $options = OPENSSL_RAW_DATA;
165
        }
166
167
        $plaintext = openssl_decrypt(
168
            $ciphertext,
169
            $this->cipher,
170
            $key->getMaterial(),
171
            $options,
172
            $iv,
173
            $authTag,
0 ignored issues
show
Bug introduced by
It seems like $authTag can also be of type null; however, parameter $tag of openssl_decrypt() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
            /** @scrutinizer ignore-type */ $authTag,
Loading history...
174
        );
175
176
        if ($plaintext === false) {
177
            throw new OpenSSLException('Cannot decrypt data');
178
        }
179
        return $this->useAuthTag ? $plaintext : $this->unpad($plaintext);
180
    }
181
182
183
    /**
184
     * Sign a given plaintext with this cipher and a given key.
185
     *
186
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to sign.
187
     * @param string $plaintext The original text to sign.
188
     *
189
     * @return string The (binary) signature corresponding to the given plaintext.
190
     *
191
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while signing the plaintext.
192
     */
193
    public function sign(
194
        #[\SensitiveParameter]
195
        KeyInterface $key,
196
        string $plaintext,
197
    ): string {
198
        if (!openssl_sign($plaintext, $signature, $key->getMaterial(), $this->digest)) {
199
            throw new OpenSSLException('Cannot sign data');
200
        }
201
        return $signature;
202
    }
203
204
205
    /**
206
     * Verify a signature with this cipher and a given key.
207
     *
208
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to verify.
209
     * @param string $plaintext The original signed text.
210
     * @param string $signature The (binary) signature to verify.
211
     *
212
     * @return boolean True if the signature can be verified, false otherwise.
213
     */
214
    public function verify(
215
        #[\SensitiveParameter]
216
        KeyInterface $key,
217
        string $plaintext,
218
        string $signature,
219
    ): bool {
220
        return openssl_verify($plaintext, $signature, $key->getMaterial(), $this->digest) === 1;
221
    }
222
223
224
    /**
225
     * Set the cipher to be used by the backend.
226
     *
227
     * @param string $cipher The identifier of the cipher.
228
     *
229
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the cipher is unknown or not supported.
230
     */
231
    public function setCipher(string $cipher): void
232
    {
233
        if (!isset(C::$BLOCK_CIPHER_ALGORITHMS[$cipher]) && !in_array($cipher, C::$KEY_TRANSPORT_ALGORITHMS)) {
234
            throw new InvalidArgumentException('Invalid or unknown cipher');
235
        }
236
237
        // configure the backend depending on the actual algorithm to use
238
        $this->useAuthTag = false;
239
        $this->cipher = $cipher;
240
        switch ($cipher) {
241
            case C::KEY_TRANSPORT_RSA_1_5:
242
                $this->padding = OPENSSL_PKCS1_PADDING;
243
                break;
244
            case C::KEY_TRANSPORT_OAEP:
245
            case C::KEY_TRANSPORT_OAEP_MGF1P:
246
                $this->padding = OPENSSL_PKCS1_OAEP_PADDING;
247
                break;
248
            case C::BLOCK_ENC_AES128_GCM:
249
            case C::BLOCK_ENC_AES192_GCM:
250
            case C::BLOCK_ENC_AES256_GCM:
251
                $this->useAuthTag = true;
252
                // Intentional fall-thru
253
            default:
254
                $this->cipher = C::$BLOCK_CIPHER_ALGORITHMS[$cipher];
255
                $this->blocksize = C::$BLOCK_SIZES[$cipher];
256
                $this->keysize = openssl_cipher_key_length(C::$BLOCK_CIPHER_ALGORITHMS[$cipher]);
0 ignored issues
show
Bug introduced by
The function openssl_cipher_key_length was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

256
                $this->keysize = /** @scrutinizer ignore-call */ openssl_cipher_key_length(C::$BLOCK_CIPHER_ALGORITHMS[$cipher]);
Loading history...
257
        }
258
    }
259
260
261
    /**
262
     * Set the digest algorithm to be used by this backend.
263
     *
264
     * @param string $digest The identifier of the digest algorithm.
265
     *
266
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the given digest is not valid.
267
     */
268
    public function setDigestAlg(string $digest): void
269
    {
270
        if (!isset(C::$DIGEST_ALGORITHMS[$digest])) {
271
            throw new InvalidArgumentException('Unknown digest or non-cryptographic hash function.');
272
        }
273
        $this->digest = C::$DIGEST_ALGORITHMS[$digest];
274
    }
275
276
277
    /**
278
     * Pad a plaintext using ISO 10126 padding.
279
     *
280
     * @param string $plaintext The plaintext to pad.
281
     *
282
     * @return string The padded plaintext.
283
     */
284
    public function pad(string $plaintext): string
285
    {
286
        $padchr = $this->blocksize - (mb_strlen($plaintext) % $this->blocksize);
287
        $pattern = chr($padchr);
288
        return $plaintext . str_repeat($pattern, $padchr);
289
    }
290
291
292
    /**
293
     * Remove an existing ISO 10126 padding from a given plaintext.
294
     *
295
     * @param string $plaintext The padded plaintext.
296
     *
297
     * @return string The plaintext without the padding.
298
     */
299
    public function unpad(string $plaintext): string
300
    {
301
        return substr($plaintext, 0, -ord(substr($plaintext, -1)));
302
    }
303
}
304