Passed
Push — master ( 8f49ae...b6886b )
by Tim
02:07
created

Security   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 102
c 2
b 0
f 0
dl 0
loc 281
rs 10
wmc 30

5 Methods

Rating   Name   Duplication   Size   Complexity  
A compareStrings() 0 3 1
B castKey() 0 39 6
A hash() 0 11 3
A decryptElement() 0 14 2
D doDecryptElement() 0 147 18
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\Utils;
6
7
use SimpleSAML\XMLSecurity\Constants;
8
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
9
use SimpleSAML\XMLSecurity\XMLSecEnc;
10
use SimpleSAML\XMLSecurity\XMLSecurityKey;
11
12
use function count;
13
use function hash_equals;
14
use function in_array;
15
use function openssl_pkey_get_details;
16
use function serialize;
17
use function sha1;
18
use function str_pad;
19
use function str_replace;
20
use function strlen;
21
use function strval;
22
use function substr;
23
use function trim;
24
use function var_export;
25
26
/**
27
 * A collection of security-related functions.
28
 *
29
 * @package simplesamlphp/xml-security
30
 */
31
class Security
32
{
33
    /**
34
     * Compare two strings in constant time.
35
     *
36
     * This function allows us to compare two given strings without any timing side channels
37
     * leaking information about them.
38
     *
39
     * @param string $known The reference string.
40
     * @param string $user The user-provided string to test.
41
     *
42
     * @return bool True if both strings are equal, false otherwise.
43
     */
44
    public static function compareStrings(string $known, string $user): bool
45
    {
46
        return hash_equals($known, $user);
47
    }
48
49
50
    /**
51
     * Compute the hash for some data with a given algorithm.
52
     *
53
     * @param string $alg The identifier of the algorithm to use.
54
     * @param string $data The data to digest.
55
     * @param bool $encode Whether to bas64-encode the result or not. Defaults to true.
56
     *
57
     * @return string The (binary or base64-encoded) digest corresponding to the given data.
58
     *
59
     * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If $alg is not a valid
60
     *   identifier of a supported digest algorithm.
61
     */
62
    public static function hash(string $alg, string $data, bool $encode = true): string
63
    {
64
        if (!array_key_exists($alg, Constants::$DIGEST_ALGORITHMS)) {
65
            throw new InvalidArgumentException('Unsupported digest method "' . $alg . '"');
66
        }
67
68
        $digest = hash(Constants::$DIGEST_ALGORITHMS[$alg], $data, true);
69
        if ($encode) {
70
            $digest = base64_encode($digest);
71
        }
72
        return $digest;
73
    }
74
75
76
    /**
77
     * Helper function to convert a XMLSecurityKey to the correct algorithm.
78
     *
79
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key.
80
     * @param string $algorithm The desired algorithm.
81
     * @param string $type Public or private key, defaults to public.
82
     * @return \SimpleSAML\XMLSecurity\XMLSecurityKey The new key.
83
     *
84
     * @throws \SimpleSAML\Assert\AssertionFailedException if assertions are false
85
     */
86
    public static function castKey(XMLSecurityKey $key, string $algorithm, string $type = null): XMLSecurityKey
87
    {
88
        $type = $type ?: 'public';
89
        Assert::oneOf($type, ["private", "public"]);
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\Utils\Assert was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
90
91
        // do nothing if algorithm is already the type of the key
92
        if ($key->type === $algorithm) {
93
            return $key;
94
        }
95
96
        if (
97
            !in_array(
98
                $algorithm,
99
                [
100
                    XMLSecurityKey::RSA_1_5,
101
                    XMLSecurityKey::RSA_SHA1,
102
                    XMLSecurityKey::RSA_SHA256,
103
                    XMLSecurityKey::RSA_SHA384,
104
                    XMLSecurityKey::RSA_SHA512
105
                ],
106
                true
107
            )
108
        ) {
109
            throw new Exception('Unsupported signing algorithm.');
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\Utils\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
110
        }
111
112
        /** @psalm-suppress PossiblyNullArgument */
113
        $keyInfo = openssl_pkey_get_details($key->key);
114
        if ($keyInfo === false) {
115
            throw new Exception('Unable to get key details from XMLSecurityKey.');
116
        }
117
        if (!isset($keyInfo['key'])) {
118
            throw new Exception('Missing key in public key details.');
119
        }
120
121
        $newKey = new XMLSecurityKey($algorithm, ['type' => $type]);
122
        $newKey->loadKey($keyInfo['key']);
123
124
        return $newKey;
125
    }
126
127
128
    /**
129
     * Decrypt an encrypted element.
130
     *
131
     * This is an internal helper function.
132
     *
133
     * @param \DOMElement $encryptedData The encrypted data.
134
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $inputKey The decryption key.
135
     * @param array &$blacklist Blacklisted decryption algorithms.
136
     * @throws \Exception
137
     * @return \DOMElement The decrypted element.
138
     */
139
    private static function doDecryptElement(
140
        DOMElement $encryptedData,
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\Utils\DOMElement was not found. Did you mean DOMElement? If so, make sure to prefix the type with \.
Loading history...
141
        XMLSecurityKey $inputKey,
142
        array &$blacklist
143
    ): DOMElement {
144
        $enc = new XMLSecEnc();
145
146
        $enc->setNode($encryptedData);
147
        $enc->type = $encryptedData->getAttribute("Type");
148
149
        $symmetricKey = $enc->locateKey($encryptedData);
150
        if (!$symmetricKey) {
151
            throw new Exception('Could not locate key algorithm in encrypted data.');
152
        }
153
154
        $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey);
155
        if (!$symmetricKeyInfo) {
156
            throw new Exception('Could not locate <dsig:KeyInfo> for the encrypted key.');
157
        }
158
159
        $inputKeyAlgo = $inputKey->getAlgorithm();
160
        if ($symmetricKeyInfo->isEncrypted) {
161
            $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorithm();
162
163
            if (in_array($symKeyInfoAlgo, $blacklist, true)) {
164
                throw new Exception('Algorithm disabled: ' . var_export($symKeyInfoAlgo, true));
165
            }
166
167
            if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) {
168
                /*
169
                 * The RSA key formats are equal, so loading an RSA_1_5 key
170
                 * into an RSA_OAEP_MGF1P key can be done without problems.
171
                 * We therefore pretend that the input key is an
172
                 * RSA_OAEP_MGF1P key.
173
                 */
174
                $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P;
175
            }
176
177
            /* Make sure that the input key format is the same as the one used to encrypt the key. */
178
            if ($inputKeyAlgo !== $symKeyInfoAlgo) {
179
                throw new Exception(
180
                    'Algorithm mismatch between input key and key used to encrypt ' .
181
                    ' the symmetric key for the message. Key was: ' .
182
                    var_export($inputKeyAlgo, true) . '; message was: ' .
183
                    var_export($symKeyInfoAlgo, true)
184
                );
185
            }
186
187
            /** @var XMLSecEnc $encKey */
188
            $encKey = $symmetricKeyInfo->encryptedCtx;
189
            $symmetricKeyInfo->key = $inputKey->key;
190
191
            $keySize = $symmetricKey->getSymmetricKeySize();
192
            if ($keySize === null) {
193
                /* To protect against "key oracle" attacks, we need to be able to create a
194
                 * symmetric key, and for that we need to know the key size.
195
                 */
196
                throw new Exception(
197
                    'Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, true)
198
                );
199
            }
200
201
            try {
202
                /**
203
                 * @var string $key
204
                 * @psalm-suppress UndefinedClass
205
                 */
206
                $key = $encKey->decryptKey($symmetricKeyInfo);
207
                if (strlen($key) !== $keySize) {
208
                    throw new Exception(
209
                        'Unexpected key size (' . strval(strlen($key) * 8) . 'bits) for encryption algorithm: ' .
210
                        var_export($symmetricKey->type, true)
211
                    );
212
                }
213
            } catch (Exception $e) {
214
                /* We failed to decrypt this key. Log it, and substitute a "random" key. */
215
//                Utils::getContainer()->getLogger()->error('Failed to decrypt symmetric key: ' . $e->getMessage());
216
                /* Create a replacement key, so that it looks like we fail in the same way as if the key was correctly
217
                 * padded. */
218
219
                /* We base the symmetric key on the encrypted key and private key, so that we always behave the
220
                 * same way for a given input key.
221
                 */
222
                $encryptedKey = $encKey->getCipherValue();
223
                if ($encryptedKey === null) {
224
                    throw new Exception('No CipherValue available in the encrypted element.');
225
                }
226
227
                /** @psalm-suppress PossiblyNullArgument */
228
                $pkey = openssl_pkey_get_details($symmetricKeyInfo->key);
229
                $pkey = sha1(serialize($pkey), true);
230
                $key = sha1($encryptedKey . $pkey, true);
231
232
                /* Make sure that the key has the correct length. */
233
                if (strlen($key) > $keySize) {
234
                    $key = substr($key, 0, $keySize);
235
                } elseif (strlen($key) < $keySize) {
236
                    $key = str_pad($key, $keySize);
237
                }
238
            }
239
            $symmetricKey->loadkey($key);
240
        } else {
241
            $symKeyAlgo = $symmetricKey->getAlgorithm();
242
            /* Make sure that the input key has the correct format. */
243
            if ($inputKeyAlgo !== $symKeyAlgo) {
244
                throw new Exception(
245
                    'Algorithm mismatch between input key and key in message. ' .
246
                    'Key was: ' . var_export($inputKeyAlgo, true) . '; message was: ' .
247
                    var_export($symKeyAlgo, true)
248
                );
249
            }
250
            $symmetricKey = $inputKey;
251
        }
252
253
        $algorithm = $symmetricKey->getAlgorithm();
254
        if (in_array($algorithm, $blacklist, true)) {
255
            throw new Exception('Algorithm disabled: ' . var_export($algorithm, true));
256
        }
257
258
        /**
259
         * @var string $decrypted
260
         * @psalm-suppress UndefinedClass
261
         */
262
        $decrypted = $enc->decryptNode($symmetricKey, false);
263
264
        /*
265
         * This is a workaround for the case where only a subset of the XML
266
         * tree was serialized for encryption. In that case, we may miss the
267
         * namespaces needed to parse the XML.
268
         */
269
        $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' .
270
                        'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
271
            $decrypted . '</root>';
272
273
        try {
274
            $newDoc = DOMDocumentFactory::fromString($xml);
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\Utils\DOMDocumentFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
275
        } catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\Utils\RuntimeException was not found. Did you mean RuntimeException? If so, make sure to prefix the type with \.
Loading history...
276
            throw new Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?', 0, $e);
277
        }
278
279
        /** @psalm-suppress PossiblyNullPropertyFetch */
280
        $decryptedElement = $newDoc->firstChild->firstChild;
281
        if (!($decryptedElement instanceof DOMElement)) {
282
            throw new Exception('Missing decrypted element or it was not actually a DOMElement.');
283
        }
284
285
        return $decryptedElement;
286
    }
287
288
289
    /**
290
     * Decrypt an encrypted element.
291
     *
292
     * @param \DOMElement $encryptedData The encrypted data.
293
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $inputKey The decryption key.
294
     * @param array $blacklist Blacklisted decryption algorithms.
295
     * @throws \Exception
296
     * @return \DOMElement The decrypted element.
297
     */
298
    public static function decryptElement(
299
        DOMElement $encryptedData,
300
        XMLSecurityKey $inputKey,
301
        array $blacklist = []
302
    ): DOMElement {
303
        try {
304
            return self::doDecryptElement($encryptedData, $inputKey, $blacklist);
305
        } catch (Exception $e) {
306
            /*
307
             * Something went wrong during decryption, but for security
308
             * reasons we cannot tell the user what failed.
309
             */
310
//            Utils::getContainer()->getLogger()->error('Decryption failed: ' . $e->getMessage());
311
            throw new Exception('Failed to decrypt XML element.', 0, $e);
312
        }
313
    }
314
}
315