<?php /** * Class Base32 | Classes/Application/Encoder/Base32.php. * * @author Robin 'codeFareith' von den Bergen <robinvonberg@gmx.de> * @copyright (c) 2018-2019 by Robin von den Bergen * @license http://opensource.org/licenses/gpl-license.php GNU Public License * @version 2.0.0 * * @link https://github.com/codeFareith/cf_google_authenticator * @see https://www.fareith.de * @see https://typo3.org */ namespace CodeFareith\CfGoogleAuthenticator\Application\Encoder; use CodeFareith\CfGoogleAuthenticator\Application\Exception\StringLengthViolation; use function array_unique; use function bindec; use function chr; use function chunk_split; use function count; use function explode; use function implode; use function is_int; use function ord; use function preg_replace; use function sprintf; use function str_pad; use function str_split; use function strlen; use function strpos; use function strtoupper; use function substr; use function trim; use function vsprintf; /** * Base32 encoder / decoder. * * Class for encoding / decoding in Base32. * Provides four standard character sets: * * {@see Base32::CHARSET_RFC4648} * * {@see Base32::CHARSET_ZBASE32} * * {@see Base32::CHARSET_CROCKFORD} * * {@see Base32::CHARSET_BASE32HEX} * Default pad string is '='. * However, you can define your own character set and pad string during instantiation. * In this case, the desired character set's length must be exactly 32 characters and * the desired pad string's length must be exactly one character. * * @package CodeFareith\CfGoogleAuthenticator\Application\Encoder */ class Base32 implements Encoder { public const CHARSET_RFC4648 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', CHARSET_ZBASE32 = 'ybndrfg8ejkmcpqxot1uwisza345h769', CHARSET_CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ', CHARSET_BASE32HEX = '0123456789ABCDEFGHIJKLMNOPQRSTUV'; public const LENGTH_CHARSET = 32, LENGTH_PAD_STRING = 1; protected const PAD_STRING = '='; /** * @var string */ protected $charset; /** * @var string */ protected $padString; public function __construct(string $charset = null, string $padString = null) { $charset = $charset ?? static::CHARSET_RFC4648; $padString = $padString ?? static::PAD_STRING; $this->setCharset($charset); $this->setPadString($padString); } public function setCharset(string $charset): void { $charset = $this->sanitizeCharset($charset); $this->validateCharset($charset); $this->charset = $charset; } private function sanitizeCharset(string $charset): string { $split = str_split($charset); $unique = array_unique($split); return implode('', $unique); } private function validateCharset(string $charset): void { $charsetLength = strlen($charset); if ($charsetLength !== static::LENGTH_CHARSET) { throw StringLengthViolation::requirementNotMet(static::LENGTH_CHARSET, $charsetLength); } } public function charset(): string { return $this->charset; } public function setPadString(string $padString): void { $this->validatePadString($padString); $this->padString = $padString; } private function validatePadString(string $padString): void { $padStringLength = strlen($padString); if ($padStringLength !== static::LENGTH_PAD_STRING) { throw StringLengthViolation::requirementNotMet(static::LENGTH_PAD_STRING, $padStringLength); } } public function padString(): string { return $this->padString; } public function encode(string $data): string { $result = ''; if (trim($data) !== '') { $binary = $this->convertStringToBinary($data); $array = $this->convertBinaryToArray($binary); $result = $this->convertBinariesToBase32($array); } return $result; } private function convertStringToBinary(string $data): string { $chars = str_split($data); return $this->convertCharsToBinary($chars); } private function convertCharsToBinary(array $chars): string { $result = ''; foreach ($chars as $char) { $result .= $this->convertCharToBinary($char); } return $result; } private function convertCharToBinary($char): string { $ord = ord($char); return sprintf('%08b', $ord); } private function convertBinaryToArray(string $binary): array { $chunks = $this->convertBinaryToChunks($binary, 5); return $this->arrayPadModulo($chunks, 8); } private function convertBinaryToChunks(string $binary, int $bits): array { $chunked = chunk_split($binary, $bits, ' '); $length = strlen($chunked); $trailing = substr($chunked, $length - 1); if ($trailing === ' ') { $chunked = substr($chunked, 0, $length - 1); } return explode(' ', $chunked); } private function arrayPadModulo(array $array, int $multiple, string $pad = null): array { while (count($array) % $multiple !== 0) { $array[] = $pad; } return $array; } private function convertBinariesToBase32(array $binaries): string { $result = ''; foreach ($binaries as $binary) { $result .= $this->getCharFromCharsetByBinary($binary); } return $result; } private function getCharFromCharsetByBinary($binary) { if ($binary === null) { $result = $this->padString(); } else { $index = $this->convertBinaryToDecimal($binary, 5); $result = $this->charset()[$index]; } return $result; } private function convertBinaryToDecimal(string $binary, int $length): int { $bin = str_pad( $binary, $length, 0 ); return (int) bindec($bin); } public function decode(string $data): string { $sanitized = $this->sanitizeCode($data); $result = ''; if ($sanitized !== '') { $binaries = $this->convertBase32ToBinaries($sanitized); $result = $this->convertBinariesToString($binaries); } return $result; } private function sanitizeCode(string $data): string { $pattern = vsprintf( '/[^%s]/', [ $this->charset(), ] ); $upperBase32 = strtoupper($data); return preg_replace($pattern, '', $upperBase32); } private function convertBase32ToBinaries(string $base32): array { $chars = str_split($base32); $binary = $this->getBinaryFromCharsetByChars($chars); $reduced = $this->stringReduceModulo($binary, 8); return $this->convertBinaryToChunks($reduced, 8); } private function getBinaryFromCharsetByChars(array $chars): string { $binary = ''; foreach ($chars as $char) { $binary .= $this->getBinaryFromCharsetByChar($char); } return $binary; } private function getBinaryFromCharsetByChar(string $char): string { $binary = ''; $index = strpos($this->charset(), $char); if (is_int($index)) { $binary = sprintf('%05b', $index); } return $binary; } private function stringReduceModulo(string $string, int $multiplier): string { while (strlen($string) % $multiplier !== 0) { $string = substr($string, 0, -1); } return $string; } private function convertBinariesToString(array $binaries): string { $string = ''; foreach ($binaries as $binary) { $string .= $this->convertBinaryToChar($binary); } return $string; } private function convertBinaryToChar($binary): string { $bindec = $this->convertBinaryToDecimal($binary, 8); return chr($bindec); } }