Passed
Push — master ( 82e8be...64e54a )
by Tim
01:59
created

Security::decryptElement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 14
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\Utils;
6
7
use DOMElement;
8
use Exception;
9
use RuntimeException;
10
use SimpleSAML\Assert\Assert;
11
use SimpleSAML\XML\DOMDocumentFactory;
12
use SimpleSAML\XMLSecurity\Constants;
13
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
14
use SimpleSAML\XMLSecurity\XMLSecEnc;
15
use SimpleSAML\XMLSecurity\XMLSecurityKey;
16
17
use function count;
18
use function hash_equals;
19
use function in_array;
20
use function openssl_pkey_get_details;
21
use function serialize;
22
use function sha1;
23
use function str_pad;
24
use function str_replace;
25
use function strlen;
26
use function strval;
27
use function substr;
28
use function trim;
29
use function var_export;
30
31
/**
32
 * A collection of security-related functions.
33
 *
34
 * @package simplesamlphp/xml-security
35
 */
36
class Security
37
{
38
    /**
39
     * Compare two strings in constant time.
40
     *
41
     * This function allows us to compare two given strings without any timing side channels
42
     * leaking information about them.
43
     *
44
     * @param string $known The reference string.
45
     * @param string $user The user-provided string to test.
46
     *
47
     * @return bool True if both strings are equal, false otherwise.
48
     */
49
    public static function compareStrings(string $known, string $user): bool
50
    {
51
        return hash_equals($known, $user);
52
    }
53
54
55
    /**
56
     * Compute the hash for some data with a given algorithm.
57
     *
58
     * @param string $alg The identifier of the algorithm to use.
59
     * @param string $data The data to digest.
60
     * @param bool $encode Whether to bas64-encode the result or not. Defaults to true.
61
     *
62
     * @return string The (binary or base64-encoded) digest corresponding to the given data.
63
     *
64
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $alg is not a valid
65
     *   identifier of a supported digest algorithm.
66
     */
67
    public static function hash(string $alg, string $data, bool $encode = true): string
68
    {
69
        if (!array_key_exists($alg, Constants::$DIGEST_ALGORITHMS)) {
70
            throw new InvalidArgumentException('Unsupported digest method "' . $alg . '"');
71
        }
72
73
        $digest = hash(Constants::$DIGEST_ALGORITHMS[$alg], $data, true);
74
        if ($encode) {
75
            $digest = base64_encode($digest);
76
        }
77
        return $digest;
78
    }
79
80
81
    /**
82
     * Helper function to convert a XMLSecurityKey to the correct algorithm.
83
     *
84
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key.
85
     * @param string $algorithm The desired algorithm.
86
     * @param string $type Public or private key, defaults to public.
87
     * @return \SimpleSAML\XMLSecurity\XMLSecurityKey The new key.
88
     *
89
     * @throws \SimpleSAML\Assert\AssertionFailedException if assertions are false
90
     */
91
    public static function castKey(XMLSecurityKey $key, string $algorithm, string $type = null): XMLSecurityKey
92
    {
93
        $type = $type ?: 'public';
94
        Assert::oneOf($type, ["private", "public"]);
95
96
        // do nothing if algorithm is already the type of the key
97
        if ($key->type === $algorithm) {
98
            return $key;
99
        }
100
101
        if (
102
            !in_array(
103
                $algorithm,
104
                [
105
                    XMLSecurityKey::RSA_1_5,
106
                    XMLSecurityKey::RSA_SHA1,
107
                    XMLSecurityKey::RSA_SHA256,
108
                    XMLSecurityKey::RSA_SHA384,
109
                    XMLSecurityKey::RSA_SHA512
110
                ],
111
                true
112
            )
113
        ) {
114
            throw new Exception('Unsupported signing algorithm.');
115
        }
116
117
        /** @psalm-suppress PossiblyNullArgument */
118
        $keyInfo = openssl_pkey_get_details($key->key);
119
        if ($keyInfo === false) {
120
            throw new Exception('Unable to get key details from XMLSecurityKey.');
121
        }
122
        if (!isset($keyInfo['key'])) {
123
            throw new Exception('Missing key in public key details.');
124
        }
125
126
        $newKey = new XMLSecurityKey($algorithm, ['type' => $type]);
127
        $newKey->loadKey($keyInfo['key']);
128
129
        return $newKey;
130
    }
131
132
133
    /**
134
     * Decrypt an encrypted element.
135
     *
136
     * This is an internal helper function.
137
     *
138
     * @param \DOMElement $encryptedData The encrypted data.
139
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $inputKey The decryption key.
140
     * @param array &$blacklist Blacklisted decryption algorithms.
141
     * @throws \Exception
142
     * @return \DOMElement The decrypted element.
143
     */
144
    private static function doDecryptElement(
145
        DOMElement $encryptedData,
146
        XMLSecurityKey $inputKey,
147
        array &$blacklist
148
    ): DOMElement {
149
        $enc = new XMLSecEnc();
150
151
        $enc->setNode($encryptedData);
152
        $enc->type = $encryptedData->getAttribute("Type");
153
154
        $symmetricKey = $enc->locateKey($encryptedData);
155
        if (!$symmetricKey) {
156
            throw new Exception('Could not locate key algorithm in encrypted data.');
157
        }
158
159
        $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey);
160
        if (!$symmetricKeyInfo) {
161
            throw new Exception('Could not locate <dsig:KeyInfo> for the encrypted key.');
162
        }
163
164
        $inputKeyAlgo = $inputKey->getAlgorithm();
165
        if ($symmetricKeyInfo->isEncrypted) {
166
            $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorithm();
167
168
            if (in_array($symKeyInfoAlgo, $blacklist, true)) {
169
                throw new Exception('Algorithm disabled: ' . var_export($symKeyInfoAlgo, true));
170
            }
171
172
            if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) {
173
                /*
174
                 * The RSA key formats are equal, so loading an RSA_1_5 key
175
                 * into an RSA_OAEP_MGF1P key can be done without problems.
176
                 * We therefore pretend that the input key is an
177
                 * RSA_OAEP_MGF1P key.
178
                 */
179
                $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P;
180
            }
181
182
            /* Make sure that the input key format is the same as the one used to encrypt the key. */
183
            if ($inputKeyAlgo !== $symKeyInfoAlgo) {
184
                throw new Exception(
185
                    'Algorithm mismatch between input key and key used to encrypt ' .
186
                    ' the symmetric key for the message. Key was: ' .
187
                    var_export($inputKeyAlgo, true) . '; message was: ' .
188
                    var_export($symKeyInfoAlgo, true)
189
                );
190
            }
191
192
            /** @var XMLSecEnc $encKey */
193
            $encKey = $symmetricKeyInfo->encryptedCtx;
194
            $symmetricKeyInfo->key = $inputKey->key;
195
196
            $keySize = $symmetricKey->getSymmetricKeySize();
197
            if ($keySize === null) {
198
                /* To protect against "key oracle" attacks, we need to be able to create a
199
                 * symmetric key, and for that we need to know the key size.
200
                 */
201
                throw new Exception(
202
                    'Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, true)
203
                );
204
            }
205
206
            try {
207
                /**
208
                 * @var string $key
209
                 * @psalm-suppress UndefinedClass
210
                 */
211
                $key = $encKey->decryptKey($symmetricKeyInfo);
212
                if (strlen($key) !== $keySize) {
213
                    throw new Exception(
214
                        'Unexpected key size (' . strval(strlen($key) * 8) . 'bits) for encryption algorithm: ' .
215
                        var_export($symmetricKey->type, true)
216
                    );
217
                }
218
            } catch (Exception $e) {
219
                /* We failed to decrypt this key. Log it, and substitute a "random" key. */
220
//                Utils::getContainer()->getLogger()->error('Failed to decrypt symmetric key: ' . $e->getMessage());
221
                /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly
222
                 * padded. */
223
224
                /* We base the symmetric key on the encrypted key and private key, so that we always behave the
225
                 * same way for a given input key.
226
                 */
227
                $encryptedKey = $encKey->getCipherValue();
228
                if ($encryptedKey === null) {
229
                    throw new Exception('No CipherValue available in the encrypted element.');
230
                }
231
232
                /** @psalm-suppress PossiblyNullArgument */
233
                $pkey = openssl_pkey_get_details($symmetricKeyInfo->key);
234
                $pkey = sha1(serialize($pkey), true);
235
                $key = sha1($encryptedKey . $pkey, true);
236
237
                /* Make sure that the key has the correct length. */
238
                if (strlen($key) > $keySize) {
239
                    $key = substr($key, 0, $keySize);
240
                } elseif (strlen($key) < $keySize) {
241
                    $key = str_pad($key, $keySize);
242
                }
243
            }
244
            $symmetricKey->loadkey($key);
245
        } else {
246
            $symKeyAlgo = $symmetricKey->getAlgorithm();
247
            /* Make sure that the input key has the correct format. */
248
            if ($inputKeyAlgo !== $symKeyAlgo) {
249
                throw new Exception(
250
                    'Algorithm mismatch between input key and key in message. ' .
251
                    'Key was: ' . var_export($inputKeyAlgo, true) . '; message was: ' .
252
                    var_export($symKeyAlgo, true)
253
                );
254
            }
255
            $symmetricKey = $inputKey;
256
        }
257
258
        $algorithm = $symmetricKey->getAlgorithm();
259
        if (in_array($algorithm, $blacklist, true)) {
260
            throw new Exception('Algorithm disabled: ' . var_export($algorithm, true));
261
        }
262
263
        /**
264
         * @var string $decrypted
265
         * @psalm-suppress UndefinedClass
266
         */
267
        $decrypted = $enc->decryptNode($symmetricKey, false);
268
269
        /*
270
         * This is a workaround for the case where only a subset of the XML
271
         * tree was serialized for encryption. In that case, we may miss the
272
         * namespaces needed to parse the XML.
273
         */
274
        $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' .
275
                        'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
276
            $decrypted . '</root>';
277
278
        try {
279
            $newDoc = DOMDocumentFactory::fromString($xml);
280
        } catch (RuntimeException $e) {
281
            throw new Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?', 0, $e);
282
        }
283
284
        /** @psalm-suppress PossiblyNullPropertyFetch */
285
        $decryptedElement = $newDoc->firstChild->firstChild;
286
        if (!($decryptedElement instanceof DOMElement)) {
287
            throw new Exception('Missing decrypted element or it was not actually a DOMElement.');
288
        }
289
290
        return $decryptedElement;
291
    }
292
293
294
    /**
295
     * Decrypt an encrypted element.
296
     *
297
     * @param \DOMElement $encryptedData The encrypted data.
298
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $inputKey The decryption key.
299
     * @param array $blacklist Blacklisted decryption algorithms.
300
     * @throws \Exception
301
     * @return \DOMElement The decrypted element.
302
     */
303
    public static function decryptElement(
304
        DOMElement $encryptedData,
305
        XMLSecurityKey $inputKey,
306
        array $blacklist = []
307
    ): DOMElement {
308
        try {
309
            return self::doDecryptElement($encryptedData, $inputKey, $blacklist);
310
        } catch (Exception $e) {
311
            /*
312
             * Something went wrong during decryption, but for security
313
             * reasons we cannot tell the user what failed.
314
             */
315
//            Utils::getContainer()->getLogger()->error('Decryption failed: ' . $e->getMessage());
316
            throw new Exception('Failed to decrypt XML element.', 0, $e);
317
        }
318
    }
319
}
320