<?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);
}
}