Failed Conditions
Push — v7 ( 2dbae0...d25f5c )
by Florent
01:59
created

RSACrypt::decrypt()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Encryption\Util;
15
16
use Jose\Component\Core\Util\BigInteger;
17
use Jose\Component\Core\Util\Hash;
18
use Jose\Component\Core\Util\RSAKey;
19
20
final class RSACrypt
21
{
22
    /**
23
     * Optimal Asymmetric Encryption Padding (OAEP).
24
     */
25
    public const ENCRYPTION_OAEP = 1;
26
27
    /**
28
     * Use PKCS#1 padding.
29
     */
30
    public const ENCRYPTION_PKCS1 = 2;
31
32
    /**
33
     * @param RSAKey $key
34
     * @param string $data
35
     * @param int $mode
36
     * @param null|string $hash
37
     *
38
     * @return string
39
     */
40
    public static function encrypt(RSAKey $key, string $data, int $mode, ?string $hash = null): string
41
    {
42
        switch ($mode) {
43
            case self::ENCRYPTION_OAEP:
44
                return self::encryptWithRSAOAEP($key, $data, $hash);
45
            case self::ENCRYPTION_PKCS1:
46
                return self::encryptWithRSA15($key, $data);
47
            default:
48
                throw new \InvalidArgumentException('Unsupported mode.');
49
        }
50
    }
51
52
    /**
53
     * @param RSAKey $key
54
     * @param string $plaintext
55
     * @param int $mode
56
     * @param null|string $hash
57
     *
58
     * @return string
59
     */
60
    public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string
61
    {
62
        switch ($mode) {
63
            case self::ENCRYPTION_OAEP:
64
                return self::decryptWithRSAOAEP($key, $plaintext, $hash);
65
            case self::ENCRYPTION_PKCS1:
66
                return self::decryptWithRSA15($key, $plaintext);
67
            default:
68
                throw new \InvalidArgumentException('Unsupported mode.');
69
        }
70
    }
71
72
    /**
73
     * @param RSAKey $key
74
     * @param string $data
75
     *
76
     * @return string
77
     */
78
    public static function encryptWithRSA15(RSAKey $key, string $data): string
79
    {
80
        $mLen = mb_strlen($data, '8bit');
81
82
        if ($mLen > $key->getModulusLength() - 11) {
83
            throw new \InvalidArgumentException('Message too long');
84
        }
85
86
        $psLen = $key->getModulusLength() - $mLen - 3;
87
        $ps = '';
88
        while (mb_strlen($ps, '8bit') !== $psLen) {
89
            $temp = random_bytes($psLen - mb_strlen($ps, '8bit'));
90
            $temp = str_replace("\x00", '', $temp);
91
            $ps .= $temp;
92
        }
93
        $type = 2;
94
        $data = chr(0).chr($type).$ps.chr(0).$data;
95
96
        $data = BigInteger::createFromBinaryString($data);
97
        $c = self::getRSAEP($key, $data);
98
        $c = self::convertIntegerToOctetString($c, $key->getModulusLength());
99
100
        return $c;
101
    }
102
103
    /**
104
     * @param RSAKey $key
105
     * @param string $c
106
     *
107
     * @return string
108
     */
109
    public static function decryptWithRSA15(RSAKey $key, string $c): string
110
    {
111
        if (mb_strlen($c, '8bit') !== $key->getModulusLength()) {
112
            throw new \InvalidArgumentException('Unable to decrypt');
113
        }
114
115
        $c = BigInteger::createFromBinaryString($c);
116
        $m = self::getRSADP($key, $c);
117
        $em = self::convertIntegerToOctetString($m, $key->getModulusLength());
118
        if ($em === false) {
119
            throw new \InvalidArgumentException('Unable to decrypt');
120
        }
121
122
        if (ord($em[0]) != 0 || ord($em[1]) > 2) {
123
            throw new \InvalidArgumentException('Unable to decrypt');
124
        }
125
126
        $ps = mb_substr($em, 2, mb_strpos($em, chr(0), 2, '8bit') - 2, '8bit');
127
        $m = mb_substr($em, mb_strlen($ps, '8bit') + 3, null, '8bit');
128
129
        if (mb_strlen($ps, '8bit') < 8) {
130
            throw new \InvalidArgumentException('Unable to decrypt');
131
        }
132
133
        return $m;
134
    }
135
136
    /**
137
     * Encryption.
138
     *
139
     * @param RSAKey $key
140
     * @param string $plaintext
141
     * @param string $hash_algorithm
142
     *
143
     * @return string
144
     */
145
    public static function encryptWithRSAOAEP(RSAKey $key, string $plaintext, string $hash_algorithm): string
146
    {
147
        /** @var Hash $hash */
148
        $hash = Hash::$hash_algorithm();
149
        $length = $key->getModulusLength() - 2 * $hash->getLength() - 2;
150
        if (0 >= $length) {
151
            throw new \RuntimeException();
152
        }
153
        $plaintext = str_split($plaintext, $length);
154
        $ciphertext = '';
155
        foreach ($plaintext as $m) {
156
            $ciphertext .= self::encryptRSAESOAEP($key, $m, $hash);
157
        }
158
159
        return $ciphertext;
160
    }
161
162
    /**
163
     * Decryption.
164
     *
165
     * @param RSAKey $key
166
     * @param string $ciphertext
167
     * @param string $hash_algorithm
168
     *
169
     * @return string
170
     */
171
    public static function decryptWithRSAOAEP(RSAKey $key, string $ciphertext, string $hash_algorithm): string
172
    {
173
        if (0 >= $key->getModulusLength()) {
174
            throw new \RuntimeException();
175
        }
176
        $hash = Hash::$hash_algorithm();
177
        $ciphertext = str_split($ciphertext, $key->getModulusLength());
178
        $ciphertext[count($ciphertext) - 1] = str_pad($ciphertext[count($ciphertext) - 1], $key->getModulusLength(), chr(0), STR_PAD_LEFT);
179
        $plaintext = '';
180
        foreach ($ciphertext as $c) {
181
            $temp = self::getRSAESOAEP($key, $c, $hash);
182
            $plaintext .= $temp;
183
        }
184
185
        return $plaintext;
186
    }
187
188
    /**
189
     * @param BigInteger $x
190
     * @param int        $xLen
191
     *
192
     * @return string
193
     */
194
    private static function convertIntegerToOctetString(BigInteger $x, int $xLen): string
195
    {
196
        $x = $x->toBytes();
197
        if (strlen($x) > $xLen) {
198
            throw new \RuntimeException('Invalid length.');
199
        }
200
201
        return str_pad($x, $xLen, chr(0), STR_PAD_LEFT);
202
    }
203
204
    /**
205
     * Octet-String-to-Integer primitive.
206
     *
207
     * @param string $x
208
     *
209
     * @return BigInteger
210
     */
211
    private static function convertOctetStringToInteger(string $x): BigInteger
212
    {
213
        return BigInteger::createFromBinaryString($x);
214
    }
215
216
    /**
217
     * Exponentiate with or without Chinese Remainder Theorem.
218
     * Operation with primes 'p' and 'q' is appox. 2x faster.
219
     *
220
     * @param RSAKey     $key
221
     * @param BigInteger $c
222
     *
223
     * @return BigInteger
224
     */
225
    private static function exponentiate(RSAKey $key, BigInteger $c): BigInteger
226
    {
227
        if ($key->isPublic() || empty($key->getPrimes())) {
228
            return $c->modPow($key->getExponent(), $key->getModulus());
229
        }
230
231
        $p = $key->getPrimes()[0];
232
        $q = $key->getPrimes()[1];
233
        $dP = $key->getExponents()[0];
234
        $dQ = $key->getExponents()[1];
235
        $qInv = $key->getCoefficient();
236
237
        $m1 = $c->modPow($dP, $p);
238
        $m2 = $c->modPow($dQ, $q);
239
        $h = $qInv->multiply($m1->subtract($m2)->add($p))->mod($p);
240
        $m = $m2->add($h->multiply($q));
241
242
        return $m;
243
    }
244
245
    /**
246
     * RSA EP.
247
     *
248
     * @param RSAKey     $key
249
     * @param BigInteger $m
250
     *
251
     * @return BigInteger
252
     */
253
    private static function getRSAEP(RSAKey $key, BigInteger $m): BigInteger
254
    {
255
        if ($m->compare(BigInteger::createFromDecimal(0)) < 0 || $m->compare($key->getModulus()) > 0) {
256
            throw new \RuntimeException();
257
        }
258
259
        return self::exponentiate($key, $m);
260
    }
261
262
    /**
263
     * RSA DP.
264
     *
265
     * @param RSAKey     $key
266
     * @param BigInteger $c
267
     *
268
     * @return BigInteger
269
     */
270
    private static function getRSADP(RSAKey $key, BigInteger $c): BigInteger
271
    {
272
        if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare($key->getModulus()) > 0) {
273
            throw new \RuntimeException();
274
        }
275
276
        return self::exponentiate($key, $c);
277
    }
278
279
    /**
280
     * MGF1.
281
     *
282
     * @param string $mgfSeed
283
     * @param int    $maskLen
284
     * @param Hash   $mgfHash
285
     *
286
     * @return string
287
     */
288
    private static function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string
289
    {
290
        $t = '';
291
        $count = ceil($maskLen / $mgfHash->getLength());
292
        for ($i = 0; $i < $count; ++$i) {
293
            $c = pack('N', $i);
294
            $t .= $mgfHash->hash($mgfSeed.$c);
295
        }
296
297
        return mb_substr($t, 0, $maskLen, '8bit');
298
    }
299
300
    /**
301
     * RSAES-OAEP-ENCRYPT.
302
     *
303
     * @param RSAKey $key
304
     * @param string $m
305
     * @param Hash   $hash
306
     *
307
     * @return string
308
     */
309
    private static function encryptRSAESOAEP(RSAKey $key, string $m, Hash $hash): string
310
    {
311
        $mLen = mb_strlen($m, '8bit');
312
        $lHash = $hash->hash('');
313
        $ps = str_repeat(chr(0), $key->getModulusLength() - $mLen - 2 * $hash->getLength() - 2);
314
        $db = $lHash.$ps.chr(1).$m;
315
        $seed = random_bytes($hash->getLength());
316
        $dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
317
        $maskedDB = strval($db ^ $dbMask);
318
        $seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
319
        $maskedSeed = $seed ^ $seedMask;
320
        $em = chr(0).$maskedSeed.$maskedDB;
321
322
        $m = self::convertOctetStringToInteger($em);
323
        $c = self::getRSAEP($key, $m);
324
        $c = self::convertIntegerToOctetString($c, $key->getModulusLength());
325
326
        return $c;
327
    }
328
329
    /**
330
     * RSAES-OAEP-DECRYPT.
331
     *
332
     * @param RSAKey $key
333
     * @param string $c
334
     * @param Hash   $hash
335
     *
336
     * @return string
337
     */
338
    private static function getRSAESOAEP(RSAKey $key, string $c, Hash $hash): string
339
    {
340
        $c = self::convertOctetStringToInteger($c);
341
        $m = self::getRSADP($key, $c);
342
        $em = self::convertIntegerToOctetString($m, $key->getModulusLength());
343
        $lHash = $hash->hash('');
344
        $maskedSeed = mb_substr($em, 1, $hash->getLength(), '8bit');
345
        $maskedDB = mb_substr($em, $hash->getLength() + 1, null, '8bit');
346
        $seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
347
        $seed = strval($maskedSeed ^ $seedMask);
348
        $dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
349
        $db = $maskedDB ^ $dbMask;
350
        $lHash2 = mb_substr($db, 0, $hash->getLength(), '8bit');
351
        $m = mb_substr($db, $hash->getLength(), null, '8bit');
352
        if (!hash_equals($lHash, $lHash2)) {
353
            throw new \RuntimeException();
354
        }
355
        $m = ltrim($m, chr(0));
356
        if (1 !== ord($m[0])) {
357
            throw new \RuntimeException();
358
        }
359
360
        return mb_substr($m, 1, null, '8bit');
361
    }
362
}
363