Failed Conditions
Push — master ( d40a11...28d61e )
by Florent
07:16
created

RSACrypt::convertIntegerToOctetString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 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
/**
21
 * @internal
22
 */
23
class RSACrypt
24
{
25
    /**
26
     * Optimal Asymmetric Encryption Padding (OAEP).
27
     */
28
    public const ENCRYPTION_OAEP = 1;
29
30
    /**
31
     * Use PKCS#1 padding.
32
     */
33
    public const ENCRYPTION_PKCS1 = 2;
34
35
    /**
36
     * @param RSAKey      $key
37
     * @param string      $data
38
     * @param int         $mode
39
     * @param null|string $hash
40
     *
41
     * @return string
42
     */
43
    public static function encrypt(RSAKey $key, string $data, int $mode, ?string $hash = null): string
44
    {
45
        switch ($mode) {
46
            case self::ENCRYPTION_OAEP:
47
                return self::encryptWithRSAOAEP($key, $data, $hash);
48
            case self::ENCRYPTION_PKCS1:
49
                return self::encryptWithRSA15($key, $data);
50
            default:
51
                throw new \InvalidArgumentException('Unsupported mode.');
52
        }
53
    }
54
55
    /**
56
     * @param RSAKey      $key
57
     * @param string      $plaintext
58
     * @param int         $mode
59
     * @param null|string $hash
60
     *
61
     * @return string
62
     */
63
    public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string
64
    {
65
        switch ($mode) {
66
            case self::ENCRYPTION_OAEP:
67
                return self::decryptWithRSAOAEP($key, $plaintext, $hash);
68
            case self::ENCRYPTION_PKCS1:
69
                return self::decryptWithRSA15($key, $plaintext);
70
            default:
71
                throw new \InvalidArgumentException('Unsupported mode.');
72
        }
73
    }
74
75
    /**
76
     * @param RSAKey $key
77
     * @param string $data
78
     *
79
     * @return string
80
     */
81
    public static function encryptWithRSA15(RSAKey $key, string $data): string
82
    {
83
        $mLen = mb_strlen($data, '8bit');
84
85
        if ($mLen > $key->getModulusLength() - 11) {
86
            throw new \InvalidArgumentException('Message too long');
87
        }
88
89
        $psLen = $key->getModulusLength() - $mLen - 3;
90
        $ps = '';
91
        while (mb_strlen($ps, '8bit') !== $psLen) {
92
            $temp = random_bytes($psLen - mb_strlen($ps, '8bit'));
93
            $temp = str_replace("\x00", '', $temp);
94
            $ps .= $temp;
95
        }
96
        $type = 2;
97
        $data = chr(0).chr($type).$ps.chr(0).$data;
98
99
        $data = BigInteger::createFromBinaryString($data);
100
        $c = self::getRSAEP($key, $data);
101
        $c = self::convertIntegerToOctetString($c, $key->getModulusLength());
102
103
        return $c;
104
    }
105
106
    /**
107
     * @param RSAKey $key
108
     * @param string $c
109
     *
110
     * @return string
111
     */
112
    public static function decryptWithRSA15(RSAKey $key, string $c): string
113
    {
114
        if (mb_strlen($c, '8bit') !== $key->getModulusLength()) {
115
            throw new \InvalidArgumentException('Unable to decrypt');
116
        }
117
118
        $c = BigInteger::createFromBinaryString($c);
119
        $m = self::getRSADP($key, $c);
120
        $em = self::convertIntegerToOctetString($m, $key->getModulusLength());
121
122
        if (0 != ord($em[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 (mb_strlen($x, '8bit') > $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
     * RSA EP.
218
     *
219
     * @param RSAKey     $key
220
     * @param BigInteger $m
221
     *
222
     * @return BigInteger
223
     */
224
    private static function getRSAEP(RSAKey $key, BigInteger $m): BigInteger
225
    {
226
        if ($m->compare(BigInteger::createFromDecimal(0)) < 0 || $m->compare($key->getModulus()) > 0) {
0 ignored issues
show
Documentation introduced by
$key->getModulus() is of type object<Jose\Component\Core\Util\BigInteger>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
227
            throw new \RuntimeException();
228
        }
229
230
        return RSAKey::exponentiate($key, $m);
0 ignored issues
show
Documentation introduced by
$key is of type object<Jose\Component\Core\Util\RSAKey>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
231
    }
232
233
    /**
234
     * RSA DP.
235
     *
236
     * @param RSAKey     $key
237
     * @param BigInteger $c
238
     *
239
     * @return BigInteger
240
     */
241
    private static function getRSADP(RSAKey $key, BigInteger $c): BigInteger
242
    {
243
        if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare($key->getModulus()) > 0) {
0 ignored issues
show
Documentation introduced by
$key->getModulus() is of type object<Jose\Component\Core\Util\BigInteger>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
244
            throw new \RuntimeException();
245
        }
246
247
        return RSAKey::exponentiate($key, $c);
0 ignored issues
show
Documentation introduced by
$key is of type object<Jose\Component\Core\Util\RSAKey>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
248
    }
249
250
    /**
251
     * MGF1.
252
     *
253
     * @param string $mgfSeed
254
     * @param int    $maskLen
255
     * @param Hash   $mgfHash
256
     *
257
     * @return string
258
     */
259
    private static function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string
260
    {
261
        $t = '';
262
        $count = ceil($maskLen / $mgfHash->getLength());
263
        for ($i = 0; $i < $count; $i++) {
264
            $c = pack('N', $i);
265
            $t .= $mgfHash->hash($mgfSeed.$c);
266
        }
267
268
        return mb_substr($t, 0, $maskLen, '8bit');
269
    }
270
271
    /**
272
     * RSAES-OAEP-ENCRYPT.
273
     *
274
     * @param RSAKey $key
275
     * @param string $m
276
     * @param Hash   $hash
277
     *
278
     * @return string
279
     */
280
    private static function encryptRSAESOAEP(RSAKey $key, string $m, Hash $hash): string
281
    {
282
        $mLen = mb_strlen($m, '8bit');
283
        $lHash = $hash->hash('');
284
        $ps = str_repeat(chr(0), $key->getModulusLength() - $mLen - 2 * $hash->getLength() - 2);
285
        $db = $lHash.$ps.chr(1).$m;
286
        $seed = random_bytes($hash->getLength());
287
        $dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
288
        $maskedDB = strval($db ^ $dbMask);
289
        $seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
290
        $maskedSeed = $seed ^ $seedMask;
291
        $em = chr(0).$maskedSeed.$maskedDB;
292
293
        $m = self::convertOctetStringToInteger($em);
294
        $c = self::getRSAEP($key, $m);
295
        $c = self::convertIntegerToOctetString($c, $key->getModulusLength());
296
297
        return $c;
298
    }
299
300
    /**
301
     * RSAES-OAEP-DECRYPT.
302
     *
303
     * @param RSAKey $key
304
     * @param string $c
305
     * @param Hash   $hash
306
     *
307
     * @return string
308
     */
309
    private static function getRSAESOAEP(RSAKey $key, string $c, Hash $hash): string
310
    {
311
        $c = self::convertOctetStringToInteger($c);
312
        $m = self::getRSADP($key, $c);
313
        $em = self::convertIntegerToOctetString($m, $key->getModulusLength());
314
        $lHash = $hash->hash('');
315
        $maskedSeed = mb_substr($em, 1, $hash->getLength(), '8bit');
316
        $maskedDB = mb_substr($em, $hash->getLength() + 1, null, '8bit');
317
        $seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
318
        $seed = strval($maskedSeed ^ $seedMask);
319
        $dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
320
        $db = $maskedDB ^ $dbMask;
321
        $lHash2 = mb_substr($db, 0, $hash->getLength(), '8bit');
322
        $m = mb_substr($db, $hash->getLength(), null, '8bit');
323
        if (!hash_equals($lHash, $lHash2)) {
324
            throw new \RuntimeException();
325
        }
326
        $m = ltrim($m, chr(0));
327
        if (1 !== ord($m[0])) {
328
            throw new \RuntimeException();
329
        }
330
331
        return mb_substr($m, 1, null, '8bit');
332
    }
333
}
334