Passed
Push — master ( 8460b0...5a07d0 )
by Tim
02:16
created

OpenSSL::setDigestAlg()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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