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
![]() |
|||
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 |