teqneers /
shamir
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
$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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
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
Loading history...
|
|||||||
| 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 |