1 | <?php |
||||||
2 | |||||||
3 | namespace TQ\Shamir\Algorithm; |
||||||
4 | |||||||
5 | use OutOfRangeException; |
||||||
6 | use RuntimeException; |
||||||
7 | use TQ\Shamir\Random\Generator; |
||||||
8 | use TQ\Shamir\Random\PhpGenerator; |
||||||
9 | |||||||
10 | /** |
||||||
11 | * Class Shamir |
||||||
12 | * |
||||||
13 | * Based on "Shamir's Secret Sharing class" from Kenny Millington |
||||||
14 | * |
||||||
15 | * @link https://www.kennynet.co.uk/misc/shamir.class.txt |
||||||
16 | * |
||||||
17 | * @package TQ\Shamir\Algorithm |
||||||
18 | */ |
||||||
19 | class Shamir implements Algorithm, RandomGeneratorAware |
||||||
20 | { |
||||||
21 | /** |
||||||
22 | * Calculation base (decimal) |
||||||
23 | * |
||||||
24 | * Changing this will invalid all previously created keys. |
||||||
25 | * |
||||||
26 | * @const string |
||||||
27 | */ |
||||||
28 | protected const DECIMAL = '0123456789'; |
||||||
29 | |||||||
30 | /** |
||||||
31 | * Target base characters to be used in passwords (shares) |
||||||
32 | * |
||||||
33 | * The more characters are used, the shorter the shares might get. |
||||||
34 | * Changing this will invalid all previously created keys. |
||||||
35 | * |
||||||
36 | * @const string |
||||||
37 | */ |
||||||
38 | protected const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz.,:;-+*#%'; |
||||||
39 | |||||||
40 | /** |
||||||
41 | * Character to fill up the secret keys |
||||||
42 | * |
||||||
43 | * @const string |
||||||
44 | */ |
||||||
45 | protected const PAD_CHAR = '='; |
||||||
46 | |||||||
47 | /** |
||||||
48 | * Prime number has to be greater than the maximum number of shares possible |
||||||
49 | * |
||||||
50 | * @var float |
||||||
51 | */ |
||||||
52 | protected $prime = 257; |
||||||
53 | |||||||
54 | /** |
||||||
55 | * Chunk size in bytes |
||||||
56 | * |
||||||
57 | * The secret will be divided equally. This value defines the chunk size and |
||||||
58 | * how many bytes will get encoded at once. |
||||||
59 | * |
||||||
60 | * @var int |
||||||
61 | */ |
||||||
62 | protected $chunkSize = 1; |
||||||
63 | |||||||
64 | /** |
||||||
65 | * The random generator |
||||||
66 | * |
||||||
67 | * @var Generator |
||||||
68 | */ |
||||||
69 | protected $randomGenerator; |
||||||
70 | |||||||
71 | /** |
||||||
72 | * Maximum number of shares required |
||||||
73 | * |
||||||
74 | * @var float |
||||||
75 | */ |
||||||
76 | protected $maxShares = 3; |
||||||
77 | |||||||
78 | /** |
||||||
79 | * @inheritdoc |
||||||
80 | */ |
||||||
81 | 19 | public function getRandomGenerator(): Generator |
|||||
82 | { |
||||||
83 | 19 | if (!$this->randomGenerator) { |
|||||
84 | 14 | $this->randomGenerator = new PhpGenerator(); |
|||||
85 | } |
||||||
86 | |||||||
87 | 19 | return $this->randomGenerator; |
|||||
88 | } |
||||||
89 | |||||||
90 | /** |
||||||
91 | * @inheritdoc |
||||||
92 | */ |
||||||
93 | 63 | public function setRandomGenerator(Generator $generator): Shamir |
|||||
94 | { |
||||||
95 | 63 | $this->randomGenerator = $generator; |
|||||
96 | |||||||
97 | 63 | return $this; |
|||||
98 | } |
||||||
99 | |||||||
100 | /** |
||||||
101 | * Returns chunk size in bytes |
||||||
102 | */ |
||||||
103 | 14 | public function getChunkSize(): int |
|||||
104 | { |
||||||
105 | 14 | return $this->chunkSize; |
|||||
106 | } |
||||||
107 | |||||||
108 | /** |
||||||
109 | * Sets chunk size in bytes |
||||||
110 | * |
||||||
111 | * If maximum shares have been set already, the chunk |
||||||
112 | * size might have been set with it. It is not possible |
||||||
113 | * to set a smaller size than required by shares. |
||||||
114 | * |
||||||
115 | * @see |
||||||
116 | * @param int $chunkSize Size in number of bytes |
||||||
117 | * @throws OutOfRangeException |
||||||
118 | */ |
||||||
119 | 36 | public function setChunkSize(int $chunkSize): Shamir |
|||||
120 | { |
||||||
121 | 36 | $primeNumber = [1 => 257, 65537, 16777259, 4294967311, 1099511627791, 281474976710677, 72057594037928017]; |
|||||
122 | |||||||
123 | 36 | if (!isset($primeNumber[$chunkSize])) { |
|||||
124 | 4 | throw new OutOfRangeException( |
|||||
125 | 4 | 'Chunk size with '.$chunkSize.' bytes is not allowed. Use 1 to '.count($primeNumber).'.' |
|||||
126 | 4 | ); |
|||||
127 | } |
||||||
128 | |||||||
129 | 32 | $this->chunkSize = $chunkSize; |
|||||
130 | // if chunk size has been set already, we will only increase it, if necessary |
||||||
131 | 32 | $this->prime = $primeNumber[$chunkSize]; |
|||||
132 | |||||||
133 | 32 | return $this; |
|||||
134 | } |
||||||
135 | |||||||
136 | /** |
||||||
137 | * Configure encoding parameters |
||||||
138 | * |
||||||
139 | * Depending on the number of required shares, we need to change |
||||||
140 | * prime number, key length, chunk size and more. |
||||||
141 | * |
||||||
142 | * If the chunk size has been set already, it will be changed, if |
||||||
143 | * it is smaller than the necessary size. |
||||||
144 | * |
||||||
145 | * @see setChunkSize() |
||||||
146 | * @param int $max Maximum number of keys needed |
||||||
147 | * @throws OutOfRangeException |
||||||
148 | */ |
||||||
149 | 19 | protected function setMaxShares(int $max): Shamir |
|||||
150 | { |
||||||
151 | // the prime number has to be larger, than the maximum number |
||||||
152 | // representable by the number of bytes. so we always need one |
||||||
153 | // byte more for encryption. if someone wants to use 256 shares, |
||||||
154 | // we could encrypt 256 with a single byte, but due to encrypting |
||||||
155 | // with a bigger prime number, we will need to use 2 bytes. |
||||||
156 | |||||||
157 | // max possible number of shares is the maximum number of bytes |
||||||
158 | // possible to be represented with max integer, but we always need |
||||||
159 | // to save one byte for encryption. |
||||||
160 | 19 | $maxPossible = 1 << (PHP_INT_SIZE - 1) * 8; |
|||||
161 | |||||||
162 | 19 | if ($max > $maxPossible) { |
|||||
163 | // we are unable to provide more bytes-1 as supported by OS |
||||||
164 | // because the prime number need to be higher than that, but |
||||||
165 | // this would exceed OS integer range. |
||||||
166 | throw new OutOfRangeException( |
||||||
167 | 'Number of required keys has to be below '.number_format($maxPossible).'.' |
||||||
168 | ); |
||||||
169 | } |
||||||
170 | |||||||
171 | // calculate how many bytes we need to represent the number of shares. |
||||||
172 | // e.g., everything less than 256 needs only a single byte. |
||||||
173 | 19 | $chunkSize = (int)ceil(log($max, 2) / 8); |
|||||
174 | // if chunk size has been set already, we will only increase it, if necessary |
||||||
175 | 19 | $chunkSize = max($chunkSize, $this->chunkSize); |
|||||
176 | |||||||
177 | 19 | if ($chunkSize > $this->chunkSize) { |
|||||
178 | 2 | $this->setChunkSize($chunkSize); |
|||||
179 | } |
||||||
180 | |||||||
181 | 19 | $this->maxShares = $max; |
|||||
182 | |||||||
183 | 19 | return $this; |
|||||
184 | } |
||||||
185 | |||||||
186 | /** |
||||||
187 | * Calculate modulo of any given number using prime |
||||||
188 | * |
||||||
189 | * @return int Module of number |
||||||
190 | */ |
||||||
191 | 18 | protected function modulo(string $number): int |
|||||
192 | { |
||||||
193 | 18 | $modulo = bcmod($number, $this->prime); |
|||||
194 | |||||||
195 | 18 | return ($modulo < 0) ? bcadd($modulo, $this->prime) : $modulo; |
|||||
196 | } |
||||||
197 | |||||||
198 | /** |
||||||
199 | * Returns decomposition of the greatest common divisor of a and b |
||||||
200 | */ |
||||||
201 | 18 | protected function gcdD(int $a, string $b): array |
|||||
202 | { |
||||||
203 | 18 | if ($b === '0') { |
|||||
204 | 18 | return [$a, 1, 0]; |
|||||
205 | } |
||||||
206 | |||||||
207 | 18 | $div = floor(bcdiv($a, $b)); |
|||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
208 | 18 | $mod = bcmod($a, $b); |
|||||
209 | 18 | $decomp = $this->gcdD($b, $mod); |
|||||
0 ignored issues
–
show
$b of type string is incompatible with the type integer expected by parameter $a of TQ\Shamir\Algorithm\Shamir::gcdD() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
210 | |||||||
211 | 18 | return [$decomp[0], $decomp[2], $decomp[1] - $decomp[2] * $div]; |
|||||
212 | } |
||||||
213 | |||||||
214 | /** |
||||||
215 | * Calculates the inverse modulo |
||||||
216 | */ |
||||||
217 | 18 | protected function inverseModulo(int $number): string |
|||||
218 | { |
||||||
219 | 18 | $mod = bcmod($number, $this->prime); |
|||||
220 | 18 | $r = $this->gcdD($this->prime, abs($mod)); |
|||||
0 ignored issues
–
show
$mod of type null|string is incompatible with the type double|integer expected by parameter $num of abs() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() $this->prime of type double is incompatible with the type integer expected by parameter $a of TQ\Shamir\Algorithm\Shamir::gcdD() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
221 | 18 | $r = ($mod < 0) ? -$r[2] : $r[2]; |
|||||
222 | |||||||
223 | 18 | return bcmod(bcadd($this->prime, $r), $this->prime); |
|||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * Calculates the reverse coefficients |
||||||
228 | * |
||||||
229 | * @throws RuntimeException |
||||||
230 | */ |
||||||
231 | 18 | protected function reverseCoefficients(array $keyX, int $threshold): array |
|||||
232 | { |
||||||
233 | 18 | $coefficients = []; |
|||||
234 | |||||||
235 | 18 | for ($i = 0; $i < $threshold; $i++) { |
|||||
236 | 18 | $temp = 1; |
|||||
237 | 18 | for ($j = 0; $j < $threshold; $j++) { |
|||||
238 | 18 | if ($i !== $j) { |
|||||
239 | 18 | $temp = $this->modulo( |
|||||
240 | 18 | bcmul(bcmul(-$temp, $keyX[$j]), $this->inverseModulo($keyX[$i] - $keyX[$j])) |
|||||
241 | 18 | ); |
|||||
242 | } |
||||||
243 | } |
||||||
244 | |||||||
245 | 18 | if ($temp === 0) { |
|||||
246 | /* Repeated share */ |
||||||
247 | throw new RuntimeException('Repeated share detected - cannot compute reverse-coefficients'); |
||||||
248 | } |
||||||
249 | |||||||
250 | 18 | $coefficients[] = $temp; |
|||||
251 | } |
||||||
252 | |||||||
253 | 18 | return $coefficients; |
|||||
254 | } |
||||||
255 | |||||||
256 | /** |
||||||
257 | * Generate random coefficients |
||||||
258 | * |
||||||
259 | * @param int $threshold Number of coefficients needed |
||||||
260 | */ |
||||||
261 | 18 | protected function generateCoefficients(int $threshold): array |
|||||
262 | { |
||||||
263 | 18 | $coefficients = []; |
|||||
264 | 18 | for ($i = 0; $i < $threshold - 1; $i++) { |
|||||
265 | do { |
||||||
266 | // the random number has to be positive integer != 0 |
||||||
267 | 18 | $random = abs($this->getRandomGenerator()->getRandomInt()); |
|||||
268 | 18 | } while ($random < 1); |
|||||
269 | 18 | $coefficients[] = $this->modulo($random); |
|||||
270 | } |
||||||
271 | |||||||
272 | 18 | return $coefficients; |
|||||
273 | } |
||||||
274 | |||||||
275 | /** |
||||||
276 | * Calculate y values of polynomial curve using horner's method |
||||||
277 | * |
||||||
278 | * Horner converts a polynomial formula like |
||||||
279 | * 11 + 7x - 5x^2 - 4x^3 + 2x^4 |
||||||
280 | * into a more efficient formula |
||||||
281 | * 11 + x * ( 7 + x * ( -5 + x * ( -4 + x * 2 ) ) ) |
||||||
282 | * |
||||||
283 | * @see https://en.wikipedia.org/wiki/Horner%27s_method |
||||||
284 | * @param int $xCoordinate X coordinate |
||||||
285 | * @param array $coefficients Polynomial coefficients |
||||||
286 | * @return int Y coordinate |
||||||
287 | */ |
||||||
288 | 18 | protected function hornerMethod(int $xCoordinate, array $coefficients): int |
|||||
289 | { |
||||||
290 | 18 | $yCoordinate = 0; |
|||||
291 | 18 | foreach ($coefficients as $coefficient) { |
|||||
292 | 18 | $yCoordinate = $this->modulo($xCoordinate * $yCoordinate + $coefficient); |
|||||
293 | } |
||||||
294 | |||||||
295 | 18 | return $yCoordinate; |
|||||
296 | } |
||||||
297 | |||||||
298 | /** |
||||||
299 | * Converts from $fromBaseInput to $toBaseInput |
||||||
300 | */ |
||||||
301 | 39 | protected static function convBase(string $numberInput, string $fromBaseInput, string $toBaseInput): string |
|||||
302 | { |
||||||
303 | 39 | if ($fromBaseInput === $toBaseInput) { |
|||||
304 | 3 | return $numberInput; |
|||||
305 | } |
||||||
306 | 36 | $fromBase = str_split($fromBaseInput, 1); |
|||||
307 | 36 | $toBase = str_split($toBaseInput, 1); |
|||||
308 | 36 | $number = str_split($numberInput, 1); |
|||||
309 | 36 | $fromLen = strlen($fromBaseInput); |
|||||
310 | 36 | $toLen = strlen($toBaseInput); |
|||||
311 | 36 | $numberLen = strlen($numberInput); |
|||||
312 | 36 | $retVal = ''; |
|||||
313 | 36 | if ($toBaseInput === '0123456789') { |
|||||
314 | 27 | $retVal = 0; |
|||||
315 | 27 | for ($i = 1; $i <= $numberLen; $i++) { |
|||||
316 | 27 | $retVal = bcadd( |
|||||
317 | 27 | $retVal, |
|||||
318 | 27 | bcmul(array_search($number[$i - 1], $fromBase, true), bcpow($fromLen, $numberLen - $i)) |
|||||
0 ignored issues
–
show
It seems like
$fromBase can also be of type true ; however, parameter $haystack of array_search() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
319 | 27 | ); |
|||||
320 | } |
||||||
321 | |||||||
322 | 27 | return $retVal; |
|||||
323 | } |
||||||
324 | 27 | if ($fromBaseInput !== '0123456789') { |
|||||
325 | $base10 = self::convBase($numberInput, $fromBaseInput, '0123456789'); |
||||||
326 | } else { |
||||||
327 | 27 | $base10 = $numberInput; |
|||||
328 | } |
||||||
329 | 27 | if ($base10 < strlen($toBaseInput)) { |
|||||
330 | 21 | return $toBase[$base10]; |
|||||
331 | } |
||||||
332 | 24 | while ($base10 !== '0') { |
|||||
333 | 24 | $retVal = $toBase[bcmod($base10, $toLen)].$retVal; |
|||||
334 | 24 | $base10 = bcdiv($base10, $toLen, 0); |
|||||
335 | } |
||||||
336 | |||||||
337 | 24 | return $retVal; |
|||||
338 | } |
||||||
339 | |||||||
340 | /** |
||||||
341 | * Unpack a binary string and convert it into decimals |
||||||
342 | * |
||||||
343 | * Convert each chunk of a binary data into decimal numbers. |
||||||
344 | * |
||||||
345 | * @param string $string Binary string |
||||||
346 | * @return array Array with decimal converted numbers |
||||||
347 | */ |
||||||
348 | 18 | protected function unpack(string $string): array |
|||||
349 | { |
||||||
350 | 18 | $chunk = 0; |
|||||
351 | 18 | $int = ''; |
|||||
352 | 18 | $return = []; |
|||||
353 | 18 | foreach (unpack('C*', $string) as $byte) { |
|||||
354 | 18 | $int = bcadd($int, bcmul($byte, bcpow(2, $chunk * 8))); |
|||||
355 | 18 | if (++$chunk === $this->chunkSize) { |
|||||
356 | 18 | $return[] = $int; |
|||||
357 | 18 | $chunk = 0; |
|||||
358 | 18 | $int = ''; |
|||||
359 | } |
||||||
360 | } |
||||||
361 | 18 | if ($chunk > 0) { |
|||||
362 | 7 | $return[] = $int; |
|||||
363 | } |
||||||
364 | |||||||
365 | 18 | return $return; |
|||||
366 | } |
||||||
367 | |||||||
368 | /** |
||||||
369 | * Returns maximum length of converted string to new base |
||||||
370 | * |
||||||
371 | * Calculate the maximum length of a string, which can be |
||||||
372 | * represented with the number of given bytes and convert |
||||||
373 | * its base. |
||||||
374 | * |
||||||
375 | * @param int $bytes Bytes used to represent a string |
||||||
376 | * @return int Number of chars |
||||||
377 | */ |
||||||
378 | 18 | protected function maxKeyLength(int $bytes): int |
|||||
379 | { |
||||||
380 | 18 | $maxInt = bcpow(2, $bytes * 8); |
|||||
381 | 18 | $converted = self::convBase($maxInt, self::DECIMAL, self::CHARS); |
|||||
382 | |||||||
383 | 18 | return strlen($converted); |
|||||
384 | } |
||||||
385 | |||||||
386 | /** |
||||||
387 | * Divide secret into chunks and calculate coordinates |
||||||
388 | * |
||||||
389 | * @param string $secret Secret |
||||||
390 | * @param int $shares Number of parts to share |
||||||
391 | * @param int $threshold Minimum number of shares required for decryption |
||||||
392 | */ |
||||||
393 | 18 | protected function divideSecret(string $secret, int $shares, int $threshold): array |
|||||
394 | { |
||||||
395 | // divide secret into chunks, which we encrypt one by one |
||||||
396 | 18 | $result = []; |
|||||
397 | |||||||
398 | 18 | foreach ($this->unpack($secret) as $bytes) { |
|||||
399 | 18 | $coeffs = $this->generateCoefficients($threshold); |
|||||
400 | 18 | $coeffs[] = $bytes; |
|||||
401 | |||||||
402 | // go through x coordinates and calculate y value |
||||||
403 | 18 | for ($x = 1; $x <= $shares; $x++) { |
|||||
404 | // use horner method to calculate y value |
||||||
405 | 18 | $result[] = $this->hornerMethod($x, $coeffs); |
|||||
406 | } |
||||||
407 | } |
||||||
408 | |||||||
409 | 18 | return $result; |
|||||
410 | } |
||||||
411 | |||||||
412 | /** |
||||||
413 | * @inheritdoc |
||||||
414 | */ |
||||||
415 | 19 | public function share($secret, $shares, $threshold = 2): array |
|||||
416 | { |
||||||
417 | 19 | $this->setMaxShares($shares); |
|||||
418 | |||||||
419 | // check if number of shares is less than our prime, otherwise we have a security problem |
||||||
420 | 19 | if ($shares >= $this->prime || $shares < 1) { |
|||||
421 | throw new OutOfRangeException('Number of shares has to be between 1 and '.$this->prime.'.'); |
||||||
422 | } |
||||||
423 | |||||||
424 | 19 | if ($shares < $threshold) { |
|||||
425 | 1 | throw new OutOfRangeException('Threshold has to be between 1 and '.$shares.'.'); |
|||||
426 | } |
||||||
427 | |||||||
428 | 18 | if (strpos(self::CHARS, self::PAD_CHAR) !== false) { |
|||||
429 | throw new OutOfRangeException('Padding character must not be part of possible encryption chars.'); |
||||||
430 | } |
||||||
431 | |||||||
432 | // divide secret into chunks, which we encrypt one by one |
||||||
433 | 18 | $result = $this->divideSecret($secret, $shares, $threshold); |
|||||
434 | |||||||
435 | // encode number of bytes and threshold |
||||||
436 | |||||||
437 | // calculate the maximum length of key sequence number and threshold |
||||||
438 | 18 | $maxBaseLength = $this->maxKeyLength($this->chunkSize); |
|||||
439 | // in order to do a correct padding to the converted base, we need to use the first char of the base |
||||||
440 | 18 | $paddingChar = substr(self::CHARS, 0, 1); |
|||||
441 | // define prefix number using the number of bytes (hex), and a left padded string used for threshold (base converted) |
||||||
442 | 18 | $fixPrefixFormat = '%x%'.$paddingChar.$maxBaseLength.'s'; |
|||||
443 | // prefix is going to be the same for all keys |
||||||
444 | 18 | $prefix = sprintf($fixPrefixFormat, $this->chunkSize, self::convBase($threshold, self::DECIMAL, self::CHARS)); |
|||||
445 | |||||||
446 | // convert y coordinates into hexadecimals shares |
||||||
447 | 18 | $passwords = []; |
|||||
448 | 18 | $secretLen = strlen($secret); |
|||||
449 | // calculate how many bytes, we need to cut off during recovery |
||||||
450 | 18 | if ($secretLen % $this->chunkSize > 0) { |
|||||
451 | 7 | $tail = str_repeat(self::PAD_CHAR, $this->chunkSize - $secretLen % $this->chunkSize); |
|||||
452 | } else { |
||||||
453 | 12 | $tail = ''; |
|||||
454 | } |
||||||
455 | |||||||
456 | 18 | $chunks = ceil($secretLen / $this->chunkSize); |
|||||
457 | 18 | for ($i = 0; $i < $shares; ++$i) { |
|||||
458 | 18 | $sequence = self::convBase(($i + 1), self::DECIMAL, self::CHARS); |
|||||
459 | 18 | $key = sprintf($prefix.'%'.$paddingChar.$maxBaseLength.'s', $sequence); |
|||||
460 | |||||||
461 | 18 | for ($j = 0; $j < $chunks; ++$j) { |
|||||
462 | 18 | $key .= str_pad( |
|||||
463 | 18 | self::convBase($result[$j * $shares + $i], self::DECIMAL, self::CHARS), |
|||||
464 | 18 | $maxBaseLength, |
|||||
465 | 18 | $paddingChar, |
|||||
466 | 18 | STR_PAD_LEFT |
|||||
467 | 18 | ); |
|||||
468 | } |
||||||
469 | 18 | $passwords[] = $key.$tail; |
|||||
470 | } |
||||||
471 | |||||||
472 | 18 | return $passwords; |
|||||
473 | } |
||||||
474 | |||||||
475 | /** |
||||||
476 | * Decode and merge secret chunks |
||||||
477 | * |
||||||
478 | * @param array $keyX Keys for X coordinates |
||||||
479 | * @param array $keyY Keys for Y coordinates |
||||||
480 | * @param int $bytes Chunk size in bytes |
||||||
481 | * @param int $keyLen Key length in chunks |
||||||
482 | * @param int $threshold Minimum number of shares required for decryption |
||||||
483 | */ |
||||||
484 | 18 | protected function joinSecret(array $keyX, array $keyY, int $bytes, int $keyLen, int $threshold): string |
|||||
485 | { |
||||||
486 | 18 | $coefficients = $this->reverseCoefficients($keyX, $threshold); |
|||||
487 | |||||||
488 | 18 | $secret = ''; |
|||||
489 | 18 | for ($i = 0; $i < $keyLen; $i++) { |
|||||
490 | 18 | $temp = 0; |
|||||
491 | 18 | for ($j = 0; $j < $threshold; $j++) { |
|||||
492 | 18 | $temp = $this->modulo( |
|||||
493 | 18 | bcadd($temp, bcmul($keyY[$j * $keyLen + $i], $coefficients[$j])) |
|||||
494 | 18 | ); |
|||||
495 | } |
||||||
496 | // convert each byte back into char |
||||||
497 | 18 | for ($byte = 1; $byte <= $bytes; ++$byte) { |
|||||
498 | 18 | $char = bcmod($temp, 256); |
|||||
499 | 18 | $secret .= chr($char); |
|||||
0 ignored issues
–
show
$char of type null|string is incompatible with the type integer expected by parameter $codepoint of chr() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
500 | 18 | $temp = bcdiv(bcsub($temp, $char), 256); |
|||||
501 | } |
||||||
502 | } |
||||||
503 | |||||||
504 | 18 | return $secret; |
|||||
505 | } |
||||||
506 | |||||||
507 | /** |
||||||
508 | * @inheritdoc |
||||||
509 | */ |
||||||
510 | 18 | public function recover(array $keys): string |
|||||
511 | { |
||||||
512 | 18 | if (!count($keys)) { |
|||||
513 | throw new RuntimeException('No keys given.'); |
||||||
514 | } |
||||||
515 | |||||||
516 | 18 | $keyX = []; |
|||||
517 | 18 | $keyY = []; |
|||||
518 | 18 | $keyLen = null; |
|||||
519 | 18 | $threshold = null; |
|||||
520 | |||||||
521 | // analyse first key |
||||||
522 | 18 | $key = reset($keys); |
|||||
523 | // first we need to find out the bytes to predict threshold and sequence length |
||||||
524 | 18 | $bytes = hexdec(substr($key, 0, 1)); |
|||||
525 | 18 | $this->setChunkSize($bytes); |
|||||
0 ignored issues
–
show
It seems like
$bytes can also be of type double ; however, parameter $chunkSize of TQ\Shamir\Algorithm\Shamir::setChunkSize() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
526 | // calculate the maximum length of key sequence number and threshold |
||||||
527 | 18 | $maxBaseLength = $this->maxKeyLength($bytes); |
|||||
528 | // define key format: bytes (hex), threshold, sequence, and key (except of bytes, all is base converted) |
||||||
529 | 18 | $keyFormat = '%1x%'.$maxBaseLength.'s%'.$maxBaseLength.'s%s'; |
|||||
530 | |||||||
531 | 18 | foreach ($keys as $key) { |
|||||
532 | // remove trailing padding characters |
||||||
533 | 18 | $key = str_replace(self::PAD_CHAR, '', $key); |
|||||
534 | |||||||
535 | // extract "public" information of key: bytes, threshold, sequence |
||||||
536 | |||||||
537 | 18 | [$keyBytes, $keyThreshold, $keySequence, $key] = sscanf($key, $keyFormat); |
|||||
538 | 18 | $keyThreshold = (int)self::convBase($keyThreshold, self::CHARS, self::DECIMAL); |
|||||
539 | 18 | $keySequence = (int)self::convBase($keySequence, self::CHARS, self::DECIMAL); |
|||||
540 | |||||||
541 | 18 | if ($threshold === null) { |
|||||
542 | 18 | $threshold = $keyThreshold; |
|||||
543 | |||||||
544 | 18 | if ($threshold > count($keys)) { |
|||||
545 | 18 | throw new RuntimeException('Not enough keys to disclose secret.'); |
|||||
546 | } |
||||||
547 | 18 | } elseif ($threshold !== $keyThreshold || $bytes != hexdec($keyBytes)) { |
|||||
548 | throw new RuntimeException('Given keys are incompatible.'); |
||||||
549 | } |
||||||
550 | |||||||
551 | 18 | $keyX[] = $keySequence; |
|||||
552 | 18 | if ($keyLen === null) { |
|||||
553 | 18 | $keyLen = strlen($key); |
|||||
554 | 18 | } elseif ($keyLen !== strlen($key)) { |
|||||
555 | throw new RuntimeException('Given keys vary in key length.'); |
||||||
556 | } |
||||||
557 | 18 | for ($i = 0; $i < $keyLen; $i += $maxBaseLength) { |
|||||
558 | 18 | $keyY[] = self::convBase(substr($key, $i, $maxBaseLength), self::CHARS, self::DECIMAL); |
|||||
559 | } |
||||||
560 | } |
||||||
561 | |||||||
562 | 18 | $keyLen /= $maxBaseLength; |
|||||
563 | 18 | $secret = $this->joinSecret($keyX, $keyY, $bytes, $keyLen, $threshold); |
|||||
0 ignored issues
–
show
It seems like
$threshold can also be of type null ; however, parameter $threshold of TQ\Shamir\Algorithm\Shamir::joinSecret() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() It seems like
$keyLen can also be of type null ; however, parameter $keyLen of TQ\Shamir\Algorithm\Shamir::joinSecret() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
564 | |||||||
565 | // remove padding from secret (NULL bytes); |
||||||
566 | 18 | $padCount = substr_count(reset($keys), self::PAD_CHAR); |
|||||
567 | 18 | if ($padCount) { |
|||||
568 | 7 | $secret = substr($secret, 0, -1 * $padCount); |
|||||
569 | } |
||||||
570 | |||||||
571 | 18 | return $secret; |
|||||
572 | } |
||||||
573 | } |
||||||
574 |