OpenSSL::encrypt()   B
last analyzed

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_decrypt;
19
use function openssl_encrypt;
20
use function openssl_sign;
21
use function openssl_verify;
22
use function ord;
23
use function str_repeat;
24
use function substr;
25
26
/**
27
 * Backend for encryption and digital signatures based on the native openssl library.
28
 *
29
 * @package SimpleSAML\XMLSecurity\Backend
30
 */
31
final class OpenSSL implements EncryptionBackend, SignatureBackend
32
{
33
    // digital signature options
34
    /** @var string */
35
    protected string $digest;
36
37
    // asymmetric encryption options
38
    /** @var int */
39
    protected int $padding = OPENSSL_PKCS1_OAEP_PADDING;
40
41
    // symmetric encryption options
42
    /** @var string */
43
    protected string $cipher;
44
45
    /** @var int */
46
    protected int $blocksize;
47
48
    /** @var int */
49
    protected int $keysize;
50
51
    /** @var bool */
52
    protected bool $useAuthTag = false;
53
54
    /** @var int */
55
    public const AUTH_TAG_LEN = 16;
56
57
58
    /**
59
     * Build a new OpenSSL backend.
60
     */
61
    public function __construct()
62
    {
63
        $this->setDigestAlg(C::DIGEST_SHA256);
64
        $this->setCipher(C::BLOCK_ENC_AES128_GCM);
65
    }
66
67
68
    /**
69
     * Encrypt a given plaintext with this cipher and a given key.
70
     *
71
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to encrypt.
72
     * @param string $plaintext The original text to encrypt.
73
     *
74
     * @return string The encrypted plaintext (ciphertext).
75
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while encrypting the plaintext.
76
     */
77
    public function encrypt(
78
        #[\SensitiveParameter]
79
        KeyInterface $key,
80
        string $plaintext,
81
    ): string {
82
        if ($key instanceof AsymmetricKey) {
83
            // asymmetric encryption
84
            $fn = 'openssl_public_encrypt';
85
            if ($key instanceof PrivateKey) {
86
                $fn = 'openssl_private_encrypt';
87
            }
88
89
            $ciphertext = '';
90
            if (!$fn($plaintext, $ciphertext, $key->getMaterial(), $this->padding)) {
91
                throw new OpenSSLException('Cannot encrypt data');
92
            }
93
            return $ciphertext;
94
        }
95
96
        // symmetric encryption
97
        $ivlen = openssl_cipher_iv_length($this->cipher);
98
        $iv = Random::generateRandomBytes($ivlen);
99
        $data = $this->pad($plaintext);
100
        $authTag = null;
101
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
102
        if ($this->useAuthTag) { // configure GCM mode
103
            $authTag = Random::generateRandomBytes(self::AUTH_TAG_LEN);
104
            $options = OPENSSL_RAW_DATA;
105
            $data = $plaintext;
106
        }
107
        $ciphertext = openssl_encrypt(
108
            $data,
109
            $this->cipher,
110
            $key->getMaterial(),
111
            $options,
112
            $iv,
113
            $authTag,
114
        );
115
116
        if (!$ciphertext) {
117
            throw new OpenSSLException('Cannot encrypt data');
118
        }
119
        return $iv . $ciphertext . $authTag;
120
    }
121
122
123
    /**
124
     * Decrypt a given ciphertext with this cipher and a given key.
125
     *
126
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to decrypt.
127
     * @param string $ciphertext The encrypted text to decrypt.
128
     *
129
     * @return string The decrypted ciphertext (plaintext).
130
     *
131
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while decrypting the ciphertext.
132
     */
133
    public function decrypt(
134
        #[\SensitiveParameter]
135
        KeyInterface $key,
136
        string $ciphertext,
137
    ): string {
138
        if ($key instanceof AsymmetricKey) {
139
            // asymmetric encryption
140
            $fn = 'openssl_public_decrypt';
141
            if ($key instanceof PrivateKey) {
142
                $fn = 'openssl_private_decrypt';
143
            }
144
145
            $plaintext = '';
146
            if (!$fn($ciphertext, $plaintext, $key->getMaterial(), $this->padding)) {
147
                throw new OpenSSLException('Cannot decrypt data');
148
            }
149
            return $plaintext;
150
        }
151
152
        // symmetric encryption
153
        $ivlen = openssl_cipher_iv_length($this->cipher);
154
        $iv = substr($ciphertext, 0, $ivlen);
155
        $ciphertext = substr($ciphertext, $ivlen);
156
157
        $authTag = null;
158
        $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
159
        if ($this->useAuthTag) { // configure GCM mode
160
            $authTag = substr($ciphertext, - self::AUTH_TAG_LEN);
161
            $ciphertext = substr($ciphertext, 0, - self::AUTH_TAG_LEN);
162
            $options = OPENSSL_RAW_DATA;
163
        }
164
165
        $plaintext = openssl_decrypt(
166
            $ciphertext,
167
            $this->cipher,
168
            $key->getMaterial(),
169
            $options,
170
            $iv,
171
            $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

171
            /** @scrutinizer ignore-type */ $authTag,
Loading history...
172
        );
173
174
        if ($plaintext === false) {
175
            throw new OpenSSLException('Cannot decrypt data');
176
        }
177
        return $this->useAuthTag ? $plaintext : $this->unpad($plaintext);
178
    }
179
180
181
    /**
182
     * Sign a given plaintext with this cipher and a given key.
183
     *
184
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to sign.
185
     * @param string $plaintext The original text to sign.
186
     *
187
     * @return string The (binary) signature corresponding to the given plaintext.
188
     *
189
     * @throws \SimpleSAML\XMLSecurity\Exception\OpenSSLException If there is an error while signing the plaintext.
190
     */
191
    public function sign(
192
        #[\SensitiveParameter]
193
        KeyInterface $key,
194
        string $plaintext,
195
    ): string {
196
        if (!openssl_sign($plaintext, $signature, $key->getMaterial(), $this->digest)) {
197
            throw new OpenSSLException('Cannot sign data');
198
        }
199
        return $signature;
200
    }
201
202
203
    /**
204
     * Verify a signature with this cipher and a given key.
205
     *
206
     * @param \SimpleSAML\XMLSecurity\Key\KeyInterface $key The key to use to verify.
207
     * @param string $plaintext The original signed text.
208
     * @param string $signature The (binary) signature to verify.
209
     *
210
     * @return boolean True if the signature can be verified, false otherwise.
211
     */
212
    public function verify(
213
        #[\SensitiveParameter]
214
        KeyInterface $key,
215
        string $plaintext,
216
        string $signature,
217
    ): bool {
218
        return openssl_verify($plaintext, $signature, $key->getMaterial(), $this->digest) === 1;
219
    }
220
221
222
    /**
223
     * Set the cipher to be used by the backend.
224
     *
225
     * @param string $cipher The identifier of the cipher.
226
     *
227
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the cipher is unknown or not supported.
228
     */
229
    public function setCipher(string $cipher): void
230
    {
231
        if (!isset(C::$BLOCK_CIPHER_ALGORITHMS[$cipher]) && !in_array($cipher, C::$KEY_TRANSPORT_ALGORITHMS)) {
232
            throw new InvalidArgumentException('Invalid or unknown cipher');
233
        }
234
235
        // configure the backend depending on the actual algorithm to use
236
        $this->useAuthTag = false;
237
        $this->cipher = $cipher;
238
        switch ($cipher) {
239
            case C::KEY_TRANSPORT_RSA_1_5:
240
                $this->padding = OPENSSL_PKCS1_PADDING;
241
                break;
242
            case C::KEY_TRANSPORT_OAEP:
243
            case C::KEY_TRANSPORT_OAEP_MGF1P:
244
                $this->padding = OPENSSL_PKCS1_OAEP_PADDING;
245
                break;
246
            case C::BLOCK_ENC_AES128_GCM:
247
            case C::BLOCK_ENC_AES192_GCM:
248
            case C::BLOCK_ENC_AES256_GCM:
249
                $this->useAuthTag = true;
250
                // Intentional fall-thru
251
            default:
252
                $this->cipher = C::$BLOCK_CIPHER_ALGORITHMS[$cipher];
253
                $this->blocksize = C::$BLOCK_SIZES[$cipher];
254
                $this->keysize = C::$BLOCK_CIPHER_KEY_SIZES[$cipher];
255
        }
256
    }
257
258
259
    /**
260
     * Set the digest algorithm to be used by this backend.
261
     *
262
     * @param string $digest The identifier of the digest algorithm.
263
     *
264
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the given digest is not valid.
265
     */
266
    public function setDigestAlg(string $digest): void
267
    {
268
        if (!isset(C::$DIGEST_ALGORITHMS[$digest])) {
269
            throw new InvalidArgumentException('Unknown digest or non-cryptographic hash function.');
270
        }
271
        $this->digest = C::$DIGEST_ALGORITHMS[$digest];
272
    }
273
274
275
    /**
276
     * Pad a plaintext using ISO 10126 padding.
277
     *
278
     * @param string $plaintext The plaintext to pad.
279
     *
280
     * @return string The padded plaintext.
281
     */
282
    public function pad(string $plaintext): string
283
    {
284
        $padchr = $this->blocksize - (mb_strlen($plaintext) % $this->blocksize);
285
        $pattern = chr($padchr);
286
        return $plaintext . str_repeat($pattern, $padchr);
287
    }
288
289
290
    /**
291
     * Remove an existing ISO 10126 padding from a given plaintext.
292
     *
293
     * @param string $plaintext The padded plaintext.
294
     *
295
     * @return string The plaintext without the padding.
296
     */
297
    public function unpad(string $plaintext): string
298
    {
299
        return substr($plaintext, 0, -ord(substr($plaintext, -1)));
300
    }
301
}
302