Completed
Push — master ( 9159fd...a2f733 )
by Oliver
08:55
created

Shamir::convBase()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 7.0027

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 25
cts 26
cp 0.9615
rs 8.3786
c 0
b 0
f 0
cc 7
nc 9
nop 3
crap 7.0027
1
<?php
2
3
namespace TQ\Shamir\Algorithm;
4
5
use OutOfRangeException;
6
use TQ\Shamir\Random\Generator;
7
use TQ\Shamir\Random\PhpGenerator;
8
9
/**
10
 * Class Shamir
11
 *
12
 * Based on "Shamir's Secret Sharing class" from Kenny Millington
13
 *
14
 * @link    https://www.kennynet.co.uk/misc/shamir.class.txt
15
 *
16
 * @package TQ\Shamir\Algorithm
17
 */
18
class Shamir implements Algorithm, RandomGeneratorAware
19
{
20
    /**
21
     * Calculation base (decimal)
22
     *
23
     * Changing this will invalid all previously created keys.
24
     *
25
     * @const   string
26
     */
27
    protected const DECIMAL = '0123456789';
28
29
    /**
30
     * Target base characters to be used in passwords (shares)
31
     *
32
     * The more characters are used, the shorter the shares might get.
33
     * Changing this will invalid all previously created keys.
34
     *
35
     * @const   string
36
     */
37
    protected const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz.,:;-+*#%';
38
39
    /**
40
     * Character to fill up the secret keys
41
     *
42
     * @const   string
43
     */
44
    protected const PAD_CHAR = '=';
45
46
    /**
47
     * Prime number has to be greater than the maximum number of shares possible
48
     *
49
     * @var float
50
     */
51
    protected $prime = 257;
52
53
    /**
54
     * Chunk size in bytes
55
     *
56
     * The secret will be divided equally. This value defines the chunk size and
57
     * how many bytes will get encoded at once.
58
     *
59
     * @var int
60
     */
61
    protected $chunkSize = 1;
62
63
    /**
64
     * The random generator
65
     *
66
     * @var Generator
67
     */
68
    protected $randomGenerator;
69
70
    /**
71
     * Maximum number of shares required
72
     *
73
     * @var float
74
     */
75
    protected $maxShares = 3;
76
77
    /**
78
     * @inheritdoc
79
     */
80 19
    public function getRandomGenerator(): Generator
81
    {
82 19
        if (!$this->randomGenerator) {
83 14
            $this->randomGenerator = new PhpGenerator();
84
        }
85
86 19
        return $this->randomGenerator;
87
    }
88
89
    /**
90
     * @inheritdoc
91
     * @return Shamir
92
     */
93 64
    public function setRandomGenerator(Generator $generator): Shamir
94
    {
95 64
        $this->randomGenerator = $generator;
96
97 64
        return $this;
98
    }
99
100
    /**
101
     * Returns chunk size in bytes
102
     *
103
     * @return int
104
     */
105 14
    public function getChunkSize(): int
106
    {
107 14
        return $this->chunkSize;
108
    }
109
110
    /**
111
     * Sets chunk size in bytes
112
     *
113
     * If maximum shares have been set already, the chunk
114
     * size might have been set with it. It is not possible
115
     * to set a smaller size than required by shares.
116
     *
117
     * @see
118
     * @param  int  $chunkSize  Size in number of bytes
119
     * @return Shamir
120
     * @throws OutOfRangeException
121
     */
122 37
    public function setChunkSize($chunkSize): Shamir
123
    {
124 37
        $chunkSize   = (int)$chunkSize;
125 37
        $primeNumber = [1 => 257, 65537, 16777259, 4294967311, 1099511627791, 281474976710677, 72057594037928017];
126
127 37
        if (!isset($primeNumber[$chunkSize])) {
128 5
            throw new \OutOfRangeException(
129 5
                'Chunk size with '.$chunkSize.' bytes is not allowed. Use 1 to '.count($primeNumber).'.'
130
            );
131
        }
132
133 32
        $this->chunkSize = $chunkSize;
134
        // if chunk size has been set already, we will only increase it, if necessary
135 32
        $this->prime = $primeNumber[$chunkSize];
0 ignored issues
show
Documentation Bug introduced by
The property $prime was declared of type double, but $primeNumber[$chunkSize] is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
136
137 32
        return $this;
138
    }
139
140
    /**
141
     * Configure encoding parameters
142
     *
143
     * Depending on the number of required shares, we need to change
144
     * prime number, key length, chunk size and more.
145
     *
146
     * If the chunk size has been set already, it will be changed, if
147
     * it is smaller than the necessary size.
148
     *
149
     * @see    setChunkSize()
150
     * @param  int  $max  Maximum number of keys needed
151
     * @return Shamir
152
     * @throws \OutOfRangeException
153
     */
154 19
    protected function setMaxShares($max): Shamir
155
    {
156
        // the prime number has to be larger, than the maximum number
157
        // representable by the number of bytes. so we always need one
158
        // byte more for encryption. if someone wants to use 256 shares,
159
        // we could encrypt 256 with a single byte, but due to encrypting
160
        // with a bigger prime number, we will need to use 2 bytes.
161
162
        // max possible number of shares is the maximum number of bytes
163
        // possible to be represented with max integer, but we always need
164
        // to save one byte for encryption.
165 19
        $maxPossible = 1 << (PHP_INT_SIZE - 1) * 8;
166
167 19
        if ($max > $maxPossible) {
168
            // we are unable to provide more bytes-1 as supported by OS
169
            // because the prime number need to be higher than that, but
170
            // this would exceed OS integer range.
171
            throw new OutOfRangeException(
172
                'Number of required keys has to be below '.number_format($maxPossible).'.'
173
            );
174
        }
175
176
        // calculate how many bytes we need to represent number of shares.
177
        // e.g. everything less than 256 needs only a single byte.
178 19
        $chunkSize = (int)ceil(log($max, 2) / 8);
179
        // if chunk size has been set already, we will only increase it, if necessary
180 19
        $chunkSize = max($chunkSize, $this->chunkSize);
181
182 19
        if ($chunkSize > $this->chunkSize) {
183 2
            $this->setChunkSize($chunkSize);
184
        }
185
186 19
        $this->maxShares = $max;
0 ignored issues
show
Documentation Bug introduced by
The property $maxShares was declared of type double, but $max is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
187
188 19
        return $this;
189
    }
190
191
    /**
192
     * Calculate modulo of any given number using prime
193
     *
194
     * @param  int     Number
195
     * @return int     Module of number
196
     */
197 18
    protected function modulo($number)
198
    {
199 18
        $modulo = bcmod($number, $this->prime);
200
201 18
        return ($modulo < 0) ? bcadd($modulo, $this->prime) : $modulo;
202
    }
203
204
    /**
205
     * Returns decomposition of the greatest common divisor of a and b
206
     *
207
     * @param  int  $a
208
     * @param  int  $b
209
     * @return array
210
     */
211 18
    protected function gcdD($a, $b): array
212
    {
213 18
        if ($b == 0) {
214 18
            return [$a, 1, 0];
215
        }
216
217 18
        $div    = floor(bcdiv($a, $b));
218 18
        $mod    = bcmod($a, $b);
219 18
        $decomp = $this->gcdD($b, $mod);
220
221 18
        return [$decomp[0], $decomp[2], $decomp[1] - $decomp[2] * $div];
222
    }
223
224
    /**
225
     * Calculates the inverse modulo
226
     *
227
     * @param  int  $number
228
     * @return int
229
     */
230 18
    protected function inverseModulo($number)
231
    {
232 18
        $number = bcmod($number, $this->prime);
233 18
        $r      = $this->gcdD($this->prime, abs($number));
234 18
        $r      = ($number < 0) ? -$r[2] : $r[2];
235
236 18
        return bcmod(bcadd($this->prime, $r), $this->prime);
237
    }
238
239
    /**
240
     * Calculates the reverse coefficients
241
     *
242
     * @param  array  $keyX
243
     * @param  int    $threshold
244
     * @return array
245
     * @throws \RuntimeException
246
     */
247 18
    protected function reverseCoefficients(array $keyX, $threshold): array
248
    {
249 18
        $coefficients = [];
250
251 18
        for ($i = 0; $i < $threshold; $i++) {
252 18
            $temp = 1;
253 18
            for ($j = 0; $j < $threshold; $j++) {
254 18
                if ($i != $j) {
255 18
                    $temp = $this->modulo(
256 18
                        bcmul(bcmul(-$temp, $keyX[$j]), $this->inverseModulo($keyX[$i] - $keyX[$j]))
257
                    );
258
                }
259
            }
260
261 18
            if ($temp == 0) {
262
                /* Repeated share */
263
                throw new \RuntimeException('Repeated share detected - cannot compute reverse-coefficients');
264
            }
265
266 18
            $coefficients[] = $temp;
267
        }
268
269 18
        return $coefficients;
270
    }
271
272
    /**
273
     * Generate random coefficients
274
     *
275
     * @param  int  $threshold  Number of coefficients needed
276
     *
277
     * @return array
278
     */
279 18
    protected function generateCoefficients($threshold): array
280
    {
281 18
        $coefficients = [];
282 18
        for ($i = 0; $i < $threshold - 1; $i++) {
283
            do {
284
                // the random number has to be positive integer != 0
285 18
                $random = abs($this->getRandomGenerator()->getRandomInt());
286 18
            } while ($random < 1);
287 18
            $coefficients[] = $this->modulo($random);
288
        }
289
290 18
        return $coefficients;
291
    }
292
293
    /**
294
     * Calculate y values of polynomial curve using horner's method
295
     *
296
     * Horner converts a polynomial formula like
297
     * 11 + 7x - 5x^2 - 4x^3 + 2x^4
298
     * into a more efficient formula
299
     * 11 + x * ( 7 + x * ( -5 + x * ( -4 + x * 2 ) ) )
300
     *
301
     * @see    https://en.wikipedia.org/wiki/Horner%27s_method
302
     * @param  int    $xCoordinate   X coordinate
303
     * @param  array  $coefficients  Polynomial coefficients
304
     * @return int                   Y coordinate
305
     */
306 18
    protected function hornerMethod($xCoordinate, array $coefficients)
307
    {
308 18
        $yCoordinate = 0;
309 18
        foreach ($coefficients as $coefficient) {
310 18
            $yCoordinate = $this->modulo($xCoordinate * $yCoordinate + $coefficient);
311
        }
312
313 18
        return $yCoordinate;
314
    }
315
316
    /**
317
     * Converts from $fromBaseInput to $toBaseInput
318
     *
319
     * @param  string  $numberInput
320
     * @param  string  $fromBaseInput
321
     * @param  string  $toBaseInput
322
     * @return string
323
     */
324 39
    protected static function convBase($numberInput, $fromBaseInput, $toBaseInput)
325
    {
326 39
        if ($fromBaseInput == $toBaseInput) {
327 3
            return $numberInput;
328
        }
329 36
        $fromBase  = str_split($fromBaseInput, 1);
330 36
        $toBase    = str_split($toBaseInput, 1);
331 36
        $number    = str_split($numberInput, 1);
332 36
        $fromLen   = strlen($fromBaseInput);
333 36
        $toLen     = strlen($toBaseInput);
334 36
        $numberLen = strlen($numberInput);
335 36
        $retVal    = '';
336 36
        if ($toBaseInput == '0123456789') {
337 27
            $retVal = 0;
338 27
            for ($i = 1; $i <= $numberLen; $i++) {
339 27
                $retVal = bcadd(
340 27
                    $retVal,
341 27
                    bcmul(array_search($number[$i - 1], $fromBase, true), bcpow($fromLen, $numberLen - $i))
342
                );
343
            }
344
345 27
            return $retVal;
346
        }
347 27
        if ($fromBaseInput != '0123456789') {
348
            $base10 = self::convBase($numberInput, $fromBaseInput, '0123456789');
349
        } else {
350 27
            $base10 = $numberInput;
351
        }
352 27
        if ($base10 < strlen($toBaseInput)) {
353 21
            return $toBase[$base10];
354
        }
355 24
        while ($base10 != '0') {
356 24
            $retVal = $toBase[bcmod($base10, $toLen)].$retVal;
357 24
            $base10 = bcdiv($base10, $toLen, 0);
358
        }
359
360 24
        return $retVal;
361
    }
362
363
    /**
364
     * Unpack a binary string and convert it into decimals
365
     *
366
     * Convert each chunk of a binary data into decimal numbers.
367
     *
368
     * @param  string  $string  Binary string
369
     * @return array            Array with decimal converted numbers
370
     */
371 18
    protected function unpack($string): array
372
    {
373 18
        $chunk  = 0;
374 18
        $int    = null;
375 18
        $return = [];
376 18
        foreach (unpack('C*', $string) as $byte) {
377 18
            $int = bcadd($int, bcmul($byte, bcpow(2, $chunk * 8)));
378 18
            if (++$chunk === $this->chunkSize) {
379 18
                $return[] = $int;
380 18
                $chunk    = 0;
381 18
                $int      = null;
382
            }
383
        }
384 18
        if ($chunk > 0) {
385 7
            $return[] = $int;
386
        }
387
388 18
        return $return;
389
    }
390
391
    /**
392
     * Returns maximum length of converted string to new base
393
     *
394
     * Calculate the maximum length of a string, which can be
395
     * represented with the number of given bytes and convert
396
     * its base.
397
     *
398
     * @param  int  $bytes  Bytes used to represent a string
399
     * @return int          Number of chars
400
     */
401 18
    protected function maxKeyLength($bytes): int
402
    {
403 18
        $maxInt    = bcpow(2, $bytes * 8);
404 18
        $converted = self::convBase($maxInt, self::DECIMAL, self::CHARS);
405
406 18
        return strlen($converted);
407
    }
408
409
    /**
410
     * Divide secret into chunks and calculate coordinates
411
     *
412
     * @param  string  $secret     Secret
413
     * @param  int     $shares     Number of parts to share
414
     * @param  int     $threshold  Minimum number of shares required for decryption
415
     * @return array
416
     */
417 18
    protected function divideSecret($secret, $shares, $threshold): array
418
    {
419
        // divide secret into chunks, which we encrypt one by one
420 18
        $result = [];
421
422 18
        foreach ($this->unpack($secret) as $bytes) {
423 18
            $coeffs   = $this->generateCoefficients($threshold);
424 18
            $coeffs[] = $bytes;
425
426
            // go through x coordinates and calculate y value
427 18
            for ($x = 1; $x <= $shares; $x++) {
428
                // use horner method to calculate y value
429 18
                $result[] = $this->hornerMethod($x, $coeffs);
430
            }
431
        }
432
433 18
        return $result;
434
    }
435
436
    /**
437
     * @inheritdoc
438
     */
439 19
    public function share($secret, $shares, $threshold = 2): array
440
    {
441 19
        $this->setMaxShares($shares);
442
443
        // check if number of shares is less than our prime, otherwise we have a security problem
444 19
        if ($shares >= $this->prime || $shares < 1) {
445
            throw new \OutOfRangeException('Number of shares has to be between 1 and '.$this->prime.'.');
446
        }
447
448 19
        if ($shares < $threshold) {
449 1
            throw new \OutOfRangeException('Threshold has to be between 1 and '.$shares.'.');
450
        }
451
452 18
        if (strpos(self::CHARS, self::PAD_CHAR) !== false) {
453
            throw new \OutOfRangeException('Padding character must not be part of possible encryption chars.');
454
        }
455
456
        // divide secret into chunks, which we encrypt one by one
457 18
        $result = $this->divideSecret($secret, $shares, $threshold);
458
459
        // encode number of bytes and threshold
460
461
        // calculate the maximum length of key sequence number and threshold
462 18
        $maxBaseLength = $this->maxKeyLength($this->chunkSize);
463
        // in order to do a correct padding to the converted base, we need to use the first char of the base
464 18
        $paddingChar = substr(self::CHARS, 0, 1);
465
        // define prefix number using the number of bytes (hex), and a left padded string used for threshold (base converted)
466 18
        $fixPrefixFormat = '%x%'.$paddingChar.$maxBaseLength.'s';
467
        // prefix is going to be the same for all keys
468 18
        $prefix = sprintf($fixPrefixFormat, $this->chunkSize, self::convBase($threshold, self::DECIMAL, self::CHARS));
469
470
        // convert y coordinates into hexadecimals shares
471 18
        $passwords = [];
472 18
        $secretLen = strlen($secret);
473
        // calculate how many bytes, we need to cut off during recovery
474 18
        if ($secretLen % $this->chunkSize > 0) {
475 7
            $tail = str_repeat(self::PAD_CHAR, $this->chunkSize - $secretLen % $this->chunkSize);
476
        } else {
477 12
            $tail = '';
478
        }
479
480 18
        $chunks = ceil($secretLen / $this->chunkSize);
481 18
        for ($i = 0; $i < $shares; ++$i) {
482 18
            $sequence = self::convBase(($i + 1), self::DECIMAL, self::CHARS);
483 18
            $key      = sprintf($prefix.'%'.$paddingChar.$maxBaseLength.'s', $sequence);
484
485 18
            for ($j = 0; $j < $chunks; ++$j) {
486 18
                $key .= str_pad(
487 18
                    self::convBase($result[$j * $shares + $i], self::DECIMAL, self::CHARS),
488
                    $maxBaseLength,
489
                    $paddingChar,
490 18
                    STR_PAD_LEFT
491
                );
492
            }
493 18
            $passwords[] = $key.$tail;
494
        }
495
496 18
        return $passwords;
497
    }
498
499
    /**
500
     * Decode and merge secret chunks
501
     *
502
     * @param  array  $keyX       Keys for X coordinates
503
     * @param  array  $keyY       Keys for Y coordinates
504
     * @param  int    $bytes      Chunk size in bytes
505
     * @param  int    $keyLen     Key length in chunks
506
     * @param  int    $threshold  Minimum number of shares required for decryption
507
     * @return string
508
     */
509 18
    protected function joinSecret($keyX, $keyY, $bytes, $keyLen, $threshold): string
510
    {
511 18
        $coefficients = $this->reverseCoefficients($keyX, $threshold);
512
513 18
        $secret = '';
514 18
        for ($i = 0; $i < $keyLen; $i++) {
515 18
            $temp = 0;
516 18
            for ($j = 0; $j < $threshold; $j++) {
517 18
                $temp = $this->modulo(
518 18
                    bcadd($temp, bcmul($keyY[$j * $keyLen + $i], $coefficients[$j]))
519
                );
520
            }
521
            // convert each byte back into char
522 18
            for ($byte = 1; $byte <= $bytes; ++$byte) {
523 18
                $char   = bcmod($temp, 256);
524 18
                $secret .= chr($char);
525 18
                $temp   = bcdiv(bcsub($temp, $char), 256);
526
            }
527
        }
528
529 18
        return $secret;
530
    }
531
532
    /**
533
     * @inheritdoc
534
     */
535 18
    public function recover(array $keys): string
536
    {
537 18
        if (!count($keys)) {
538
            throw new \RuntimeException('No keys given.');
539
        }
540
541 18
        $keyX      = [];
542 18
        $keyY      = [];
543 18
        $keyLen    = null;
544 18
        $threshold = null;
545
546
        // analyse first key
547 18
        $key = reset($keys);
548
        // first we need to find out the bytes to predict threshold and sequence length
549 18
        $bytes = hexdec(substr($key, 0, 1));
550 18
        $this->setChunkSize($bytes);
551
        // calculate the maximum length of key sequence number and threshold
552 18
        $maxBaseLength = $this->maxKeyLength($bytes);
553
        // define key format: bytes (hex), threshold, sequence, and key (except of bytes, all is base converted)
554 18
        $keyFormat = '%1x%'.$maxBaseLength.'s%'.$maxBaseLength.'s%s';
555
556 18
        foreach ($keys as $key) {
557
            // remove trailing padding characters
558 18
            $key = str_replace(self::PAD_CHAR, '', $key);
559
560
            // extract "public" information of key: bytes, threshold, sequence
561
562 18
            [$keyBytes, $keyThreshold, $keySequence, $key] = sscanf($key, $keyFormat);
0 ignored issues
show
Bug introduced by
The variable $keyBytes does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $keyThreshold does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $keySequence does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
563 18
            $keyThreshold = (int)self::convBase($keyThreshold, self::CHARS, self::DECIMAL);
564 18
            $keySequence  = (int)self::convBase($keySequence, self::CHARS, self::DECIMAL);
565
566 18
            if ($threshold === null) {
567 18
                $threshold = $keyThreshold;
568
569 18
                if ($threshold > count($keys)) {
570 18
                    throw new \RuntimeException('Not enough keys to disclose secret.');
571
                }
572 18
            } elseif ($threshold != $keyThreshold || $bytes != hexdec($keyBytes)) {
573
                throw new \RuntimeException('Given keys are incompatible.');
574
            }
575
576 18
            $keyX[] = $keySequence;
577 18
            if ($keyLen === null) {
578 18
                $keyLen = strlen($key);
579 18
            } elseif ($keyLen !== strlen($key)) {
580
                throw new \RuntimeException('Given keys vary in key length.');
581
            }
582 18
            for ($i = 0; $i < $keyLen; $i += $maxBaseLength) {
583 18
                $keyY[] = self::convBase(substr($key, $i, $maxBaseLength), self::CHARS, self::DECIMAL);
584
            }
585
        }
586
587 18
        $keyLen /= $maxBaseLength;
588 18
        $secret = $this->joinSecret($keyX, $keyY, $bytes, $keyLen, $threshold);
589
590
        // remove padding from secret (NULL bytes);
591 18
        $padCount = substr_count(reset($keys), self::PAD_CHAR);
592 18
        if ($padCount) {
593 7
            $secret = substr($secret, 0, -1 * $padCount);
594
        }
595
596 18
        return $secret;
597
    }
598
}
599