├── enable ├── .gitignore ├── default-disable ├── dictionaries ├── errors.definition.json ├── errors.translation.json ├── login.definition.json └── login.translation.json ├── composer.json ├── README.md ├── templates └── login.php ├── www └── login.php └── lib └── Auth └── Source └── authtfaga.php /enable: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | -------------------------------------------------------------------------------- /default-disable: -------------------------------------------------------------------------------- 1 | This file indicates that the default state of this module 2 | is disabled. To enable, create a file named enable in the 3 | same directory as this file. 4 | -------------------------------------------------------------------------------- /dictionaries/errors.definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "title_WRONGOTP": { 3 | "en": "Invalid verification code!" 4 | }, 5 | "descr_WRONGOTP": { 6 | "en": "The Google Authenticator based one time password validation failed. Try again, or contact the administrators!" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "niif/simplesamlphp-module-authtfaga", 3 | "description": "Two-factor authentication module for simpleSAMLphp using Google Authenticator", 4 | "type": "simplesamlphp-module", 5 | "require": { 6 | "simplesamlphp/composer-module-installer": "^1.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dictionaries/errors.translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "title_WRONGOTP": { 3 | "hu": "A megadott ellenőrzőkód hibás!" 4 | }, 5 | "descr_WRONGOTP": { 6 | "hu": "A Google Authenticator alapú egyszer használatos jelszó hibás! Próbáld újra, vagy vedd fel a kapcsolatot az adminisztátorral!" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Two-factor authentication module for simpleSAMLphp using Google Authenticator 2 | 3 | ## Usage 4 | 5 | Configure it by adding an entry to `config/authsources.php` such as this: 6 | 7 | ``` 8 | 'authtfaga' => array( 9 | 'authtfaga:authtfaga', 10 | 11 | 'db.dsn' => 'mysql:host=db.example.com;port=3306;dbname=idpauthtfaga', 12 | 'db.username' => 'simplesaml', 13 | 'db.password' => 'bigsecret', 14 | 'mainAuthSource' => 'ldap', 15 | 'uidField' => 'uid', 16 | 'totpIssuer' => 'dev_aai_teszt_IdP' 17 | ), 18 | ``` 19 | -------------------------------------------------------------------------------- /dictionaries/login.definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "otpfield": { 3 | "en": "Verificaton code" 4 | }, 5 | "chooseOTP": { 6 | "en": "Would you like to use two-factor authentication based on Google Authenticator?" 7 | }, 8 | "yes": { 9 | "en": "Yes" 10 | }, 11 | "no": { 12 | "en": "No" 13 | }, 14 | "next": { 15 | "en": "Next" 16 | }, 17 | "2factor_title": { 18 | "en": "Set up two-factor authentication" 19 | }, 20 | "authentication": { 21 | "en": "Authentication" 22 | }, 23 | "qrcode": { 24 | "en": "Read the QR code with Google Authenticator app, and type the verification code below" 25 | }, 26 | "verificationcode": { 27 | "en": "Type your verifying code" 28 | }, 29 | "2factor_login": { 30 | "en": "Login with verification code." 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dictionaries/login.translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "otpfield": { 3 | "hu": "Ellenőrző kód" 4 | }, 5 | "chooseOTP": { 6 | "hu": "Szeretnéd használni a jövőben Google Authenticator-féle kétfaktoros azonosítási lehetőséget?" 7 | }, 8 | "yes": { 9 | "hu": "Igen" 10 | }, 11 | "no": { 12 | "hu": "Nem" 13 | }, 14 | "next": { 15 | "hu": "Tovább" 16 | }, 17 | "2factor_title": { 18 | "hu": "Kétfaktoros azonosítás - beállítás" 19 | }, 20 | "authentication": { 21 | "hu": "Bejelentkezés" 22 | }, 23 | "qrcode": { 24 | "hu": "Olvasd be a QR-kódot a mobilodon lévő Google Authenticator alkalmazással, majd add meg az ellenőrző kódot alább" 25 | }, 26 | "verificationcode": { 27 | "hu": "Add meg az aktuális ellenőrző kódot" 28 | }, 29 | "2factor_login": { 30 | "hu": "Bejelentkezés ellenőrző kóddal" 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /templates/login.php: -------------------------------------------------------------------------------- 1 | includeAtTemplateBase('includes/header.php'); 4 | $this->data['header'] = $this->t('{authtfaga:login:authentication}'); 5 | ?> 6 | 7 | data['errorcode'] !== NULL) :?> 8 |
9 | 10 |

t('{login:error_header}'); ?>

11 |

t('{authtfaga:errors:title_' . $this->data['errorcode'] . '}'); ?>

12 |

t('{authtfaga:errors:descr_' . $this->data['errorcode'] . '}'); ?>

13 |
14 | 15 | 16 |
17 | data['todo'] == 'choose2enable' ) : ?> 18 |

t('{authtfaga:login:2factor_title}')?>

19 |
20 |

t('{authtfaga:login:chooseOTP}')?>

21 |

22 | t('{authtfaga:login:yes}')?> 23 | t('{authtfaga:login:no}')?> 24 | 25 |

26 |
27 | 28 | data['todo'] == 'generateGA' ) : ?> 29 |

t('{authtfaga:login:2factor_title}')?>

30 |
31 |

t('{authtfaga:login:qrcode}')?>

32 |

33 |

34 |

35 |
36 | 37 | data['todo'] == 'loginOTP' ) : ?> 38 |

t('{authtfaga:login:2factor_login}')?>

39 |
40 |

41 | t('{authtfaga:login:verificationcode}')?> 42 | 43 | 44 |

45 |
46 | 47 | 48 | 49 | data['stateparams'] as $name => $value) { 51 | echo(''); 52 | } 53 | ?> 54 | 55 |
56 | 57 | includeAtTemplateBase('includes/footer.php'); 59 | ?> 60 | -------------------------------------------------------------------------------- /www/login.php: -------------------------------------------------------------------------------- 1 | getValue($authId); 20 | 21 | // Use 2 factor authentication classvar_dump($authId); 22 | /** @noinspection PhpUnhandledExceptionInspection */ 23 | /** @var sspmod_authtfaga_Auth_Source_authtfaga $gaLogin */ 24 | $gaLogin = SimpleSAML_Auth_Source::getById($authId, 'sspmod_authtfaga_Auth_Source_authtfaga'); 25 | if ($gaLogin === null) { 26 | /** @noinspection PhpUnhandledExceptionInspection */ 27 | throw new Exception('Invalid authentication source: ' . $authId); 28 | } 29 | 30 | // Init template 31 | $template = 'authtfaga:login.php'; 32 | /** @noinspection PhpUnhandledExceptionInspection */ 33 | $globalConfig = SimpleSAML_Configuration::getInstance(); 34 | /** @noinspection PhpParamsInspection */ 35 | $t = new SimpleSAML_XHTML_Template($globalConfig, $template); 36 | 37 | $errorCode = null; 38 | 39 | //If user doesn't have session, force to use the main authentication method 40 | if (!$session->isValid($as['mainAuthSource'])) { 41 | /** @noinspection PhpUnhandledExceptionInspection */ 42 | $mainLogin = SimpleSAML_Auth_Source::getById($as['mainAuthSource']); 43 | $mainLogin->initLogin(SimpleSAML\Utils\HTTP::getSelfURL()); 44 | } 45 | 46 | $attributes = $session->getAuthData($as['mainAuthSource'], 'Attributes'); 47 | $state['Attributes'] = $attributes; 48 | 49 | $uid = $attributes[$as['uidField']][0]; 50 | $state['UserID'] = $uid; 51 | $isEnabled = $gaLogin->isEnabled2fa($uid); 52 | 53 | if (is_null($isEnabled) || isset($_GET['postSetEnable2fa'])) { 54 | //If the user has not set his preference of 2 factor authentication, redirect to settings page 55 | if (isset($_POST['setEnable2f'])) { 56 | if ($_POST['setEnable2f'] == 1) { 57 | $gaKey = $gaLogin->createSecret(); 58 | $gaLogin->registerGAkey($uid, $gaKey); 59 | 60 | $gaLogin->enable2fa($uid); 61 | $t->data['todo'] = 'generateGA'; 62 | $t->data['autofocus'] = 'otp'; 63 | $totpIssuer = empty($as['totpIssuer']) ? 'dev_aai_teszt_IdP' : $as['totpIssuer']; 64 | $t->data['qrcode'] = $gaLogin->getQRCodeGoogleUrl($totpIssuer . ':' . $uid, $totpIssuer, $gaKey); 65 | } elseif ($_POST['setEnable2f'] == 0) { 66 | $gaLogin->disable2fa($uid); 67 | SimpleSAML_Auth_Source::completeAuth($state); 68 | } 69 | } else { 70 | $t->data['todo'] = 'choose2enable'; 71 | } 72 | } elseif ($isEnabled == 1) { 73 | //Show the second factor form 74 | if (isset($_POST['otp'])) { 75 | $secret = $gaLogin->getGAkeyFromUID($uid); 76 | $loggedIn = $gaLogin->verifyCode($secret, $_POST['otp']); 77 | 78 | if ($loggedIn) { 79 | $state['saml:AuthnContextClassRef'] = $gaLogin->tfa_authencontextclassref; 80 | 81 | if (isset($state['Attributes']['userCertificate;binary'])) { 82 | unset($state['Attributes']['userCertificate;binary']); 83 | } 84 | 85 | SimpleSAML_Auth_Source::completeAuth($state); 86 | } else { 87 | $errorCode = 'WRONGOTP'; 88 | $t->data['todo'] = 'loginOTP'; 89 | } 90 | } else { 91 | $t->data['autofocus'] = 'otp'; 92 | $t->data['todo'] = 'loginOTP'; 93 | } 94 | } else { 95 | 96 | if (isset($state['Attributes']['userCertificate;binary'])) { 97 | unset($state['Attributes']['userCertificate;binary']); 98 | } 99 | 100 | // User has set up not to use 2 factor, so he is logged in 101 | SimpleSAML_Auth_Source::completeAuth($state); 102 | } 103 | 104 | $t->data['stateparams'] = array('AuthState' => $authStateId); 105 | $t->data['errorcode'] = $errorCode; 106 | $t->show(); 107 | exit(); 108 | -------------------------------------------------------------------------------- /lib/Auth/Source/authtfaga.php: -------------------------------------------------------------------------------- 1 | array( 11 | * 'authtfaga:authtfaga', 12 | * 13 | * 'db.dsn' => 'mysql:host=db.example.com;port=3306;dbname=idpauthtfaga', 14 | * 'db.username' => 'simplesaml', 15 | * 'db.password' => 'bigsecret', 16 | * 'mainAuthSource' => 'ldap', 17 | * 'uidField' => 'uid' 18 | * ), 19 | * 20 | */ 21 | 22 | class sspmod_authtfaga_Auth_Source_authtfaga extends SimpleSAML_Auth_Source 23 | { 24 | /** 25 | * The string used to identify our states. 26 | */ 27 | const STAGEID = 'authtfaga.stage'; 28 | 29 | /** 30 | * The number of characters of the OTP that is the secure token. 31 | * The rest is the user id. 32 | */ 33 | const TOKENSIZE = 32; 34 | 35 | /** 36 | * The key of the AuthId field in the state. 37 | */ 38 | const AUTHID = 'sspmod_authtfaga_Auth_Source_authtfaga.AuthId'; 39 | 40 | /** 41 | * sstc-saml-loa-authncontext-profile-draft.odt. 42 | */ 43 | const TFAAUTHNCONTEXTCLASSREF = 'urn:oasis:names:tc:SAML:2.0:post:ac:classes:nist-800-63:3'; 44 | 45 | private $db_dsn; 46 | private $db_username; 47 | private $db_password; 48 | private $dbh; 49 | 50 | public $tfa_authencontextclassref; 51 | 52 | //Google Authenticator code length 53 | protected $_codeLength = 6; 54 | 55 | /** 56 | * Constructor for this authentication source. 57 | * 58 | * @param array $info Information about this authentication source. 59 | * @param array $config Configuration. 60 | */ 61 | public function __construct($info, $config) 62 | { 63 | assert('is_array($info)'); 64 | assert('is_array($config)'); 65 | 66 | // Call the parent constructor first, as required by the interface. 67 | parent::__construct($info, $config); 68 | 69 | if (array_key_exists('db.dsn', $config)) { 70 | $this->db_dsn = $config['db.dsn']; 71 | } 72 | if (array_key_exists('db.username', $config)) { 73 | $this->db_username = $config['db.username']; 74 | } 75 | if (array_key_exists('db.password', $config)) { 76 | $this->db_password = $config['db.password']; 77 | } 78 | 79 | $this->tfa_authencontextclassref = self::TFAAUTHNCONTEXTCLASSREF; 80 | try { 81 | $this->dbh = new PDO($this->db_dsn, $this->db_username, $this->db_password); 82 | } catch (PDOException $e) { 83 | echo 'Connection failed: '.$e->getMessage(); 84 | } 85 | $this->createTables(); 86 | } 87 | 88 | public function authenticate(&$state) 89 | { 90 | assert('is_array($state)'); 91 | 92 | // We are going to need the authId in order to retrieve this authentication source later. 93 | $state[self::AUTHID] = $this->authId; 94 | 95 | $id = SimpleSAML_Auth_State::saveState($state, self::STAGEID); 96 | 97 | $url = SimpleSAML_Module::getModuleURL('authtfaga/login.php'); 98 | SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('AuthState' => $id)); 99 | } 100 | 101 | private function createTables() 102 | { 103 | $q = 'CREATE TABLE IF NOT EXISTS sspga_gakeys ( 104 | gakey VARCHAR (20), 105 | uid VARCHAR(60), 106 | PRIMARY KEY(gakey) 107 | );'; 108 | $this->dbh->query($q); 109 | $q = 'CREATE TABLE IF NOT EXISTS sspga_status ( 110 | uid VARCHAR(60), 111 | enable INT, 112 | PRIMARY KEY(uid) 113 | );'; 114 | $this->dbh->query($q); 115 | } 116 | 117 | public function enable2fa($uid) 118 | { 119 | $q = "REPLACE INTO sspga_status (enable, uid) VALUES (1, '$uid')"; 120 | $result = $this->dbh->query($q); 121 | if($result===false) throw new Exception('Enable TFA failed '.$q); 122 | SimpleSAML_Logger::info('authtfaga: '.$uid.' turns ON the two-factor authentication.'); 123 | 124 | return true; 125 | } 126 | 127 | public function disable2fa($uid) 128 | { 129 | $q = "REPLACE INTO sspga_status (enable, uid) VALUES (0, '$uid')"; 130 | $this->dbh->query($q); 131 | SimpleSAML_Logger::info('authtfaga: '.$uid.' turns OFF the two-factor authentication.'); 132 | 133 | return true; 134 | } 135 | 136 | public function isEnabled2fa($uid) 137 | { 138 | $q = "SELECT * FROM sspga_status WHERE uid='$uid'"; 139 | $result = $this->dbh->query($q); 140 | $row = $result->fetch(); 141 | $enabled = $row['enable']; 142 | 143 | return $enabled; 144 | } 145 | 146 | public function registerGAkey($uid, $ga_id) 147 | { 148 | if (!$ga_id) { 149 | return false; 150 | } 151 | 152 | $q = 'REPLACE INTO sspga_gakeys (uid,gakey) VALUES ("'.$uid.'","'.$ga_id.'");'; 153 | $this->dbh->query($q); 154 | SimpleSAML_Logger::info('authtfaga: '.$uid.' register his gakey: '.$ga_id); 155 | 156 | return true; 157 | } 158 | 159 | public function deletegakey($uid, $ga_id) 160 | { 161 | $q = 'DELETE FROM sspga_gakeys WHERE uid="'.$uid.'" AND gakey="'.$ga_id.'";'; 162 | $this->dbh->query($q); 163 | SimpleSAML_Logger::info('authtfaga: '.$uid.' delete his gakey: '.$ga_id); 164 | 165 | return true; 166 | } 167 | 168 | public function getGAkeyFromUID($uid) 169 | { 170 | $q = "SELECT gakey FROM sspga_gakeys WHERE uid='$uid'"; 171 | $result = $this->dbh->query($q); 172 | $row = $result->fetch(); 173 | 174 | return $row['gakey']; 175 | } 176 | 177 | /** 178 | * Below this line there is PHP Class for handling Google Authenticator 2-factor authentication. 179 | * 180 | * @author Michael Kliewe 181 | * @copyright 2012 Michael Kliewe 182 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License 183 | * 184 | * @link http://www.phpgangsta.de/ 185 | */ 186 | 187 | /** 188 | * Create new secret. 189 | * 16 characters, randomly chosen from the allowed base32 characters. 190 | * 191 | * @param int $secretLength 192 | * 193 | * @return string 194 | */ 195 | public function createSecret($secretLength = 16) 196 | { 197 | $validChars = $this->_getBase32LookupTable(); 198 | unset($validChars[32]); 199 | 200 | $secret = ''; 201 | for ($i = 0; $i < $secretLength; ++$i) { 202 | $secret .= $validChars[array_rand($validChars)]; 203 | } 204 | 205 | return $secret; 206 | } 207 | 208 | /** 209 | * Calculate the code, with given secret and point in time. 210 | * 211 | * @param string $secret 212 | * @param int|null $timeSlice 213 | * 214 | * @return string 215 | */ 216 | public function getCode($secret, $timeSlice = null) 217 | { 218 | if ($timeSlice === null) { 219 | $timeSlice = floor(time() / 30); 220 | } 221 | 222 | $secretkey = $this->_base32Decode($secret); 223 | 224 | // Pack time into binary string 225 | $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); 226 | // Hash it with users secret key 227 | $hm = hash_hmac('SHA1', $time, $secretkey, true); 228 | // Use last nipple of result as index/offset 229 | $offset = ord(substr($hm, -1)) & 0x0F; 230 | // grab 4 bytes of the result 231 | $hashpart = substr($hm, $offset, 4); 232 | 233 | // Unpak binary value 234 | $value = unpack('N', $hashpart); 235 | $value = $value[1]; 236 | // Only 32 bits 237 | $value = $value & 0x7FFFFFFF; 238 | 239 | $modulo = pow(10, $this->_codeLength); 240 | 241 | return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); 242 | } 243 | 244 | /** 245 | * Get QR-Code URL for image, from google charts. 246 | * 247 | * @param string $name 248 | * @param string $issuer 249 | * @param string $secret 250 | * 251 | * @return string 252 | */ 253 | public function getQRCodeGoogleUrl($name, $issuer, $secret) 254 | { 255 | $urlencoded = rawurlencode('otpauth://totp/'.$name.'?secret='.$secret.'&issuer='.$issuer); 256 | 257 | return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.''; 258 | } 259 | 260 | /** 261 | * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now. 262 | * 263 | * @param string $secret 264 | * @param string $code 265 | * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 266 | * 267 | * @return bool 268 | */ 269 | public function verifyCode($secret, $code, $discrepancy = 1) 270 | { 271 | $currentTimeSlice = floor(time() / 30); 272 | 273 | for ($i = -$discrepancy; $i <= $discrepancy; ++$i) { 274 | $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); 275 | if ($calculatedCode == $code) { 276 | return true; 277 | } 278 | } 279 | 280 | return false; 281 | } 282 | 283 | /** 284 | * Set the code length, should be >=6. 285 | * 286 | * @param int $length 287 | * 288 | * @return sspmod_authtfaga_Auth_Source_authtfaga 289 | */ 290 | public function setCodeLength($length) 291 | { 292 | $this->_codeLength = $length; 293 | 294 | return $this; 295 | } 296 | 297 | /** 298 | * Helper class to decode base32. 299 | * 300 | * @param $secret 301 | * 302 | * @return bool|string 303 | */ 304 | protected function _base32Decode($secret) 305 | { 306 | if (empty($secret)) { 307 | return ''; 308 | } 309 | 310 | $base32chars = $this->_getBase32LookupTable(); 311 | $base32charsFlipped = array_flip($base32chars); 312 | 313 | $paddingCharCount = substr_count($secret, $base32chars[32]); 314 | $allowedValues = array(6, 4, 3, 1, 0); 315 | if (!in_array($paddingCharCount, $allowedValues)) { 316 | return false; 317 | } 318 | for ($i = 0; $i < 4; ++$i) { 319 | if ($paddingCharCount == $allowedValues[$i] && 320 | substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) { 321 | return false; 322 | } 323 | } 324 | $secret = str_replace('=', '', $secret); 325 | $secret = str_split($secret); 326 | $binaryString = ''; 327 | for ($i = 0; $i < count($secret); $i = $i + 8) { 328 | $x = ''; 329 | if (!in_array($secret[$i], $base32chars)) { 330 | return false; 331 | } 332 | for ($j = 0; $j < 8; ++$j) { 333 | $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); 334 | } 335 | $eightBits = str_split($x, 8); 336 | for ($z = 0; $z < count($eightBits); ++$z) { 337 | $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : ''; 338 | } 339 | } 340 | 341 | return $binaryString; 342 | } 343 | 344 | /** 345 | * Helper class to encode base32. 346 | * 347 | * @param string $secret 348 | * @param bool $padding 349 | * 350 | * @return string 351 | */ 352 | protected function _base32Encode($secret, $padding = true) 353 | { 354 | if (empty($secret)) { 355 | return ''; 356 | } 357 | 358 | $base32chars = $this->_getBase32LookupTable(); 359 | 360 | $secret = str_split($secret); 361 | $binaryString = ''; 362 | for ($i = 0; $i < count($secret); ++$i) { 363 | $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); 364 | } 365 | $fiveBitBinaryArray = str_split($binaryString, 5); 366 | $base32 = ''; 367 | $i = 0; 368 | while ($i < count($fiveBitBinaryArray)) { 369 | $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; 370 | ++$i; 371 | } 372 | if ($padding && ($x = strlen($binaryString) % 40) != 0) { 373 | if ($x == 8) { 374 | $base32 .= str_repeat($base32chars[32], 6); 375 | } elseif ($x == 16) { 376 | $base32 .= str_repeat($base32chars[32], 4); 377 | } elseif ($x == 24) { 378 | $base32 .= str_repeat($base32chars[32], 3); 379 | } elseif ($x == 32) { 380 | $base32 .= $base32chars[32]; 381 | } 382 | } 383 | 384 | return $base32; 385 | } 386 | 387 | /** 388 | * Get array with all 32 characters for decoding from/encoding to base32. 389 | * 390 | * @return array 391 | */ 392 | protected function _getBase32LookupTable() 393 | { 394 | return array( 395 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 396 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 397 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 398 | 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 399 | '=', // padding char 400 | ); 401 | } 402 | } 403 | --------------------------------------------------------------------------------