PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 2.9.0
WP 2FA – Two-factor authentication for WordPress v2.9.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / bacon / bacon-qr-code / src / Encoder / Encoder.php
wp-2fa / includes / classes / bacon / bacon-qr-code / src / Encoder Last commit date
BlockPair.php 11 months ago ByteMatrix.php 11 months ago Encoder.php 11 months ago MaskUtil.php 11 months ago MatrixUtil.php 11 months ago QrCode.php 11 months ago
Encoder.php
603 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace WP2FA_Vendor\BaconQrCode\Encoder;
5
6 use WP2FA_Vendor\BaconQrCode\Common\BitArray;
7 use WP2FA_Vendor\BaconQrCode\Common\CharacterSetEci;
8 use WP2FA_Vendor\BaconQrCode\Common\ErrorCorrectionLevel;
9 use WP2FA_Vendor\BaconQrCode\Common\Mode;
10 use WP2FA_Vendor\BaconQrCode\Common\ReedSolomonCodec;
11 use WP2FA_Vendor\BaconQrCode\Common\Version;
12 use WP2FA_Vendor\BaconQrCode\Exception\WriterException;
13 use SplFixedArray;
14 /**
15 * Encoder.
16 */
17 final class Encoder
18 {
19 /**
20 * Default byte encoding.
21 */
22 public const DEFAULT_BYTE_MODE_ECODING = 'ISO-8859-1';
23 /**
24 * The original table is defined in the table 5 of JISX0510:2004 (p.19).
25 */
26 private const ALPHANUMERIC_TABLE = [
27 -1,
28 -1,
29 -1,
30 -1,
31 -1,
32 -1,
33 -1,
34 -1,
35 -1,
36 -1,
37 -1,
38 -1,
39 -1,
40 -1,
41 -1,
42 -1,
43 // 0x00-0x0f
44 -1,
45 -1,
46 -1,
47 -1,
48 -1,
49 -1,
50 -1,
51 -1,
52 -1,
53 -1,
54 -1,
55 -1,
56 -1,
57 -1,
58 -1,
59 -1,
60 // 0x10-0x1f
61 36,
62 -1,
63 -1,
64 -1,
65 37,
66 38,
67 -1,
68 -1,
69 -1,
70 -1,
71 39,
72 40,
73 -1,
74 41,
75 42,
76 43,
77 // 0x20-0x2f
78 0,
79 1,
80 2,
81 3,
82 4,
83 5,
84 6,
85 7,
86 8,
87 9,
88 44,
89 -1,
90 -1,
91 -1,
92 -1,
93 -1,
94 // 0x30-0x3f
95 -1,
96 10,
97 11,
98 12,
99 13,
100 14,
101 15,
102 16,
103 17,
104 18,
105 19,
106 20,
107 21,
108 22,
109 23,
110 24,
111 // 0x40-0x4f
112 25,
113 26,
114 27,
115 28,
116 29,
117 30,
118 31,
119 32,
120 33,
121 34,
122 35,
123 -1,
124 -1,
125 -1,
126 -1,
127 -1,
128 ];
129 /**
130 * Codec cache.
131 *
132 * @var array<string,ReedSolomonCodec>
133 */
134 private static $codecs = [];
135 /**
136 * Encodes "content" with the error correction level "ecLevel".
137 */
138 public static function encode(string $content, ErrorCorrectionLevel $ecLevel, string $encoding = self::DEFAULT_BYTE_MODE_ECODING, ?Version $forcedVersion = null) : QrCode
139 {
140 // Pick an encoding mode appropriate for the content. Note that this
141 // will not attempt to use multiple modes / segments even if that were
142 // more efficient. Would be nice.
143 $mode = self::chooseMode($content, $encoding);
144 // This will store the header information, like mode and length, as well
145 // as "header" segments like an ECI segment.
146 $headerBits = new BitArray();
147 // Append ECI segment if applicable
148 if (Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ECODING !== $encoding) {
149 $eci = CharacterSetEci::getCharacterSetEciByName($encoding);
150 if (null !== $eci) {
151 self::appendEci($eci, $headerBits);
152 }
153 }
154 // (With ECI in place,) Write the mode marker
155 self::appendModeInfo($mode, $headerBits);
156 // Collect data within the main segment, separately, to count its size
157 // if needed. Don't add it to main payload yet.
158 $dataBits = new BitArray();
159 self::appendBytes($content, $mode, $dataBits, $encoding);
160 // Hard part: need to know version to know how many bits length takes.
161 // But need to know how many bits it takes to know version. First we
162 // take a guess at version by assuming version will be the minimum, 1:
163 $provisionalBitsNeeded = $headerBits->getSize() + $mode->getCharacterCountBits(Version::getVersionForNumber(1)) + $dataBits->getSize();
164 $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
165 // Use that guess to calculate the right version. I am still not sure
166 // this works in 100% of cases.
167 $bitsNeeded = $headerBits->getSize() + $mode->getCharacterCountBits($provisionalVersion) + $dataBits->getSize();
168 $version = self::chooseVersion($bitsNeeded, $ecLevel);
169 if (null !== $forcedVersion) {
170 // Forced version check
171 if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
172 // Calculated minimum version is same or equal as forced version
173 $version = $forcedVersion;
174 } else {
175 throw new WriterException('Invalid version! Calculated version: ' . $version->getVersionNumber() . ', requested version: ' . $forcedVersion->getVersionNumber());
176 }
177 }
178 $headerAndDataBits = new BitArray();
179 $headerAndDataBits->appendBitArray($headerBits);
180 // Find "length" of main segment and write it.
181 $numLetters = Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : \strlen($content);
182 self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
183 // Put data together into the overall payload.
184 $headerAndDataBits->appendBitArray($dataBits);
185 $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
186 $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
187 // Terminate the bits properly.
188 self::terminateBits($numDataBytes, $headerAndDataBits);
189 // Interleave data bits with error correction code.
190 $finalBits = self::interleaveWithEcBytes($headerAndDataBits, $version->getTotalCodewords(), $numDataBytes, $ecBlocks->getNumBlocks());
191 // Choose the mask pattern.
192 $dimension = $version->getDimensionForVersion();
193 $matrix = new ByteMatrix($dimension, $dimension);
194 $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
195 // Build the matrix.
196 MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
197 return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
198 }
199 /**
200 * Gets the alphanumeric code for a byte.
201 */
202 private static function getAlphanumericCode(int $code) : int
203 {
204 if (isset(self::ALPHANUMERIC_TABLE[$code])) {
205 return self::ALPHANUMERIC_TABLE[$code];
206 }
207 return -1;
208 }
209 /**
210 * Chooses the best mode for a given content.
211 */
212 private static function chooseMode(string $content, ?string $encoding = null) : Mode
213 {
214 if (null !== $encoding && 0 === \strcasecmp($encoding, 'SHIFT-JIS')) {
215 return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
216 }
217 $hasNumeric = \false;
218 $hasAlphanumeric = \false;
219 $contentLength = \strlen($content);
220 for ($i = 0; $i < $contentLength; ++$i) {
221 $char = $content[$i];
222 if (\ctype_digit($char)) {
223 $hasNumeric = \true;
224 } elseif (-1 !== self::getAlphanumericCode(\ord($char))) {
225 $hasAlphanumeric = \true;
226 } else {
227 return Mode::BYTE();
228 }
229 }
230 if ($hasAlphanumeric) {
231 return Mode::ALPHANUMERIC();
232 } elseif ($hasNumeric) {
233 return Mode::NUMERIC();
234 }
235 return Mode::BYTE();
236 }
237 /**
238 * Calculates the mask penalty for a matrix.
239 */
240 private static function calculateMaskPenalty(ByteMatrix $matrix) : int
241 {
242 return MaskUtil::applyMaskPenaltyRule1($matrix) + MaskUtil::applyMaskPenaltyRule2($matrix) + MaskUtil::applyMaskPenaltyRule3($matrix) + MaskUtil::applyMaskPenaltyRule4($matrix);
243 }
244 /**
245 * Checks if content only consists of double-byte kanji characters.
246 */
247 private static function isOnlyDoubleByteKanji(string $content) : bool
248 {
249 $bytes = @\iconv('utf-8', 'SHIFT-JIS', $content);
250 if (\false === $bytes) {
251 return \false;
252 }
253 $length = \strlen($bytes);
254 if (0 !== $length % 2) {
255 return \false;
256 }
257 for ($i = 0; $i < $length; $i += 2) {
258 $byte = $bytes[$i] & 0xff;
259 if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
260 return \false;
261 }
262 }
263 return \true;
264 }
265 /**
266 * Chooses the best mask pattern for a matrix.
267 */
268 private static function chooseMaskPattern(BitArray $bits, ErrorCorrectionLevel $ecLevel, Version $version, ByteMatrix $matrix) : int
269 {
270 $minPenalty = \PHP_INT_MAX;
271 $bestMaskPattern = -1;
272 for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
273 MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
274 $penalty = self::calculateMaskPenalty($matrix);
275 if ($penalty < $minPenalty) {
276 $minPenalty = $penalty;
277 $bestMaskPattern = $maskPattern;
278 }
279 }
280 return $bestMaskPattern;
281 }
282 /**
283 * Chooses the best version for the input.
284 *
285 * @throws WriterException if data is too big
286 */
287 private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
288 {
289 for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
290 $version = Version::getVersionForNumber($versionNum);
291 $numBytes = $version->getTotalCodewords();
292 $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
293 $numEcBytes = $ecBlocks->getTotalEcCodewords();
294 $numDataBytes = $numBytes - $numEcBytes;
295 $totalInputBytes = \intdiv($numInputBits + 8, 8);
296 if ($numDataBytes >= $totalInputBytes) {
297 return $version;
298 }
299 }
300 throw new WriterException('Data too big');
301 }
302 /**
303 * Terminates the bits in a bit array.
304 *
305 * @throws WriterException if data bits cannot fit in the QR code
306 * @throws WriterException if bits size does not equal the capacity
307 */
308 private static function terminateBits(int $numDataBytes, BitArray $bits) : void
309 {
310 $capacity = $numDataBytes << 3;
311 if ($bits->getSize() > $capacity) {
312 throw new WriterException('Data bits cannot fit in the QR code');
313 }
314 for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
315 $bits->appendBit(\false);
316 }
317 $numBitsInLastByte = $bits->getSize() & 0x7;
318 if ($numBitsInLastByte > 0) {
319 for ($i = $numBitsInLastByte; $i < 8; ++$i) {
320 $bits->appendBit(\false);
321 }
322 }
323 $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
324 for ($i = 0; $i < $numPaddingBytes; ++$i) {
325 $bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
326 }
327 if ($bits->getSize() !== $capacity) {
328 throw new WriterException('Bits size does not equal capacity');
329 }
330 }
331 /**
332 * Gets number of data- and EC bytes for a block ID.
333 *
334 * @return int[]
335 * @throws WriterException if block ID is too large
336 * @throws WriterException if EC bytes mismatch
337 * @throws WriterException if RS blocks mismatch
338 * @throws WriterException if total bytes mismatch
339 */
340 private static function getNumDataBytesAndNumEcBytesForBlockId(int $numTotalBytes, int $numDataBytes, int $numRsBlocks, int $blockId) : array
341 {
342 if ($blockId >= $numRsBlocks) {
343 throw new WriterException('Block ID too large');
344 }
345 $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
346 $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
347 $numTotalBytesInGroup1 = \intdiv($numTotalBytes, $numRsBlocks);
348 $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
349 $numDataBytesInGroup1 = \intdiv($numDataBytes, $numRsBlocks);
350 $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
351 $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
352 $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
353 if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
354 throw new WriterException('EC bytes mismatch');
355 }
356 if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
357 throw new WriterException('RS blocks mismatch');
358 }
359 if ($numTotalBytes !== ($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1 + ($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2) {
360 throw new WriterException('Total bytes mismatch');
361 }
362 if ($blockId < $numRsBlocksInGroup1) {
363 return [$numDataBytesInGroup1, $numEcBytesInGroup1];
364 } else {
365 return [$numDataBytesInGroup2, $numEcBytesInGroup2];
366 }
367 }
368 /**
369 * Interleaves data with EC bytes.
370 *
371 * @throws WriterException if number of bits and data bytes does not match
372 * @throws WriterException if data bytes does not match offset
373 * @throws WriterException if an interleaving error occurs
374 */
375 private static function interleaveWithEcBytes(BitArray $bits, int $numTotalBytes, int $numDataBytes, int $numRsBlocks) : BitArray
376 {
377 if ($bits->getSizeInBytes() !== $numDataBytes) {
378 throw new WriterException('Number of bits and data bytes does not match');
379 }
380 $dataBytesOffset = 0;
381 $maxNumDataBytes = 0;
382 $maxNumEcBytes = 0;
383 $blocks = new SplFixedArray($numRsBlocks);
384 for ($i = 0; $i < $numRsBlocks; ++$i) {
385 list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId($numTotalBytes, $numDataBytes, $numRsBlocks, $i);
386 $size = $numDataBytesInBlock;
387 $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
388 $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
389 $blocks[$i] = new BlockPair($dataBytes, $ecBytes);
390 $maxNumDataBytes = \max($maxNumDataBytes, $size);
391 $maxNumEcBytes = \max($maxNumEcBytes, \count($ecBytes));
392 $dataBytesOffset += $numDataBytesInBlock;
393 }
394 if ($numDataBytes !== $dataBytesOffset) {
395 throw new WriterException('Data bytes does not match offset');
396 }
397 $result = new BitArray();
398 for ($i = 0; $i < $maxNumDataBytes; ++$i) {
399 foreach ($blocks as $block) {
400 $dataBytes = $block->getDataBytes();
401 if ($i < \count($dataBytes)) {
402 $result->appendBits($dataBytes[$i], 8);
403 }
404 }
405 }
406 for ($i = 0; $i < $maxNumEcBytes; ++$i) {
407 foreach ($blocks as $block) {
408 $ecBytes = $block->getErrorCorrectionBytes();
409 if ($i < \count($ecBytes)) {
410 $result->appendBits($ecBytes[$i], 8);
411 }
412 }
413 }
414 if ($numTotalBytes !== $result->getSizeInBytes()) {
415 throw new WriterException('Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ');
416 }
417 return $result;
418 }
419 /**
420 * Generates EC bytes for given data.
421 *
422 * @param SplFixedArray<int> $dataBytes
423 * @return SplFixedArray<int>
424 */
425 private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
426 {
427 $numDataBytes = \count($dataBytes);
428 $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
429 for ($i = 0; $i < $numDataBytes; $i++) {
430 $toEncode[$i] = $dataBytes[$i] & 0xff;
431 }
432 $ecBytes = new SplFixedArray($numEcBytesInBlock);
433 $codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
434 $codec->encode($toEncode, $ecBytes);
435 return $ecBytes;
436 }
437 /**
438 * Gets an RS codec and caches it.
439 */
440 private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
441 {
442 $cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
443 if (isset(self::$codecs[$cacheId])) {
444 return self::$codecs[$cacheId];
445 }
446 return self::$codecs[$cacheId] = new ReedSolomonCodec(8, 0x11d, 0, 1, $numEcBytesInBlock, 255 - $numDataBytes - $numEcBytesInBlock);
447 }
448 /**
449 * Appends mode information to a bit array.
450 */
451 private static function appendModeInfo(Mode $mode, BitArray $bits) : void
452 {
453 $bits->appendBits($mode->getBits(), 4);
454 }
455 /**
456 * Appends length information to a bit array.
457 *
458 * @throws WriterException if num letters is bigger than expected
459 */
460 private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
461 {
462 $numBits = $mode->getCharacterCountBits($version);
463 if ($numLetters >= 1 << $numBits) {
464 throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
465 }
466 $bits->appendBits($numLetters, $numBits);
467 }
468 /**
469 * Appends bytes to a bit array in a specific mode.
470 *
471 * @throws WriterException if an invalid mode was supplied
472 */
473 private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
474 {
475 switch ($mode) {
476 case Mode::NUMERIC():
477 self::appendNumericBytes($content, $bits);
478 break;
479 case Mode::ALPHANUMERIC():
480 self::appendAlphanumericBytes($content, $bits);
481 break;
482 case Mode::BYTE():
483 self::append8BitBytes($content, $bits, $encoding);
484 break;
485 case Mode::KANJI():
486 self::appendKanjiBytes($content, $bits);
487 break;
488 default:
489 throw new WriterException('Invalid mode: ' . $mode);
490 }
491 }
492 /**
493 * Appends numeric bytes to a bit array.
494 */
495 private static function appendNumericBytes(string $content, BitArray $bits) : void
496 {
497 $length = \strlen($content);
498 $i = 0;
499 while ($i < $length) {
500 $num1 = (int) $content[$i];
501 if ($i + 2 < $length) {
502 // Encode three numeric letters in ten bits.
503 $num2 = (int) $content[$i + 1];
504 $num3 = (int) $content[$i + 2];
505 $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
506 $i += 3;
507 } elseif ($i + 1 < $length) {
508 // Encode two numeric letters in seven bits.
509 $num2 = (int) $content[$i + 1];
510 $bits->appendBits($num1 * 10 + $num2, 7);
511 $i += 2;
512 } else {
513 // Encode one numeric letter in four bits.
514 $bits->appendBits($num1, 4);
515 ++$i;
516 }
517 }
518 }
519 /**
520 * Appends alpha-numeric bytes to a bit array.
521 *
522 * @throws WriterException if an invalid alphanumeric code was found
523 */
524 private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
525 {
526 $length = \strlen($content);
527 $i = 0;
528 while ($i < $length) {
529 $code1 = self::getAlphanumericCode(\ord($content[$i]));
530 if (-1 === $code1) {
531 throw new WriterException('Invalid alphanumeric code');
532 }
533 if ($i + 1 < $length) {
534 $code2 = self::getAlphanumericCode(\ord($content[$i + 1]));
535 if (-1 === $code2) {
536 throw new WriterException('Invalid alphanumeric code');
537 }
538 // Encode two alphanumeric letters in 11 bits.
539 $bits->appendBits($code1 * 45 + $code2, 11);
540 $i += 2;
541 } else {
542 // Encode one alphanumeric letter in six bits.
543 $bits->appendBits($code1, 6);
544 ++$i;
545 }
546 }
547 }
548 /**
549 * Appends regular 8-bit bytes to a bit array.
550 *
551 * @throws WriterException if content cannot be encoded to target encoding
552 */
553 private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
554 {
555 $bytes = @\iconv('utf-8', $encoding, $content);
556 if (\false === $bytes) {
557 throw new WriterException('Could not encode content to ' . $encoding);
558 }
559 $length = \strlen($bytes);
560 for ($i = 0; $i < $length; $i++) {
561 $bits->appendBits(\ord($bytes[$i]), 8);
562 }
563 }
564 /**
565 * Appends KANJI bytes to a bit array.
566 *
567 * @throws WriterException if content does not seem to be encoded in SHIFT-JIS
568 * @throws WriterException if an invalid byte sequence occurs
569 */
570 private static function appendKanjiBytes(string $content, BitArray $bits) : void
571 {
572 if (\strlen($content) % 2 > 0) {
573 // We just do a simple length check here. The for loop will check
574 // individual characters.
575 throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
576 }
577 $length = \strlen($content);
578 for ($i = 0; $i < $length; $i += 2) {
579 $byte1 = \ord($content[$i]) & 0xff;
580 $byte2 = \ord($content[$i + 1]) & 0xff;
581 $code = $byte1 << 8 | $byte2;
582 if ($code >= 0x8140 && $code <= 0x9ffc) {
583 $subtracted = $code - 0x8140;
584 } elseif ($code >= 0xe040 && $code <= 0xebbf) {
585 $subtracted = $code - 0xc140;
586 } else {
587 throw new WriterException('Invalid byte sequence');
588 }
589 $encoded = ($subtracted >> 8) * 0xc0 + ($subtracted & 0xff);
590 $bits->appendBits($encoded, 13);
591 }
592 }
593 /**
594 * Appends ECI information to a bit array.
595 */
596 private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
597 {
598 $mode = Mode::ECI();
599 $bits->appendBits($mode->getBits(), 4);
600 $bits->appendBits($eci->getValue(), 8);
601 }
602 }
603