[GH-ISSUE #226] Issue with Invalid characters in base32 string message when using valid base32 string #564

Open
opened 2026-03-14 12:13:10 +03:00 by kerem · 1 comment
Owner

Originally created by @brandonobrien on GitHub (May 9, 2025).
Original GitHub issue: https://github.com/antonioribeiro/google2fa/issues/226

I'm trying to migrate from sonata-project/google-authenticator since it's been abandoned for a while. I have thousands of 2fa codes set up that are based on an md5 hash + 2fa salt environment value. These are then converted to base32 and used for creating the QR code as well as validating the 6 digit pin.

The issue I'm running into is verifyKey is throwing an InvalidCharactersException even though the string is valid. I wrote a test script and verified the string is valid through several different means. I'll include as much information as I can, including the test script I wrote, that proves this issue.

  • Library Version: pragmarx/google2fa v8.0.3 (added as "pragmarx/google2fa": "^8.0" in my composer.json file
  • PHP Version: 8.3.1
  • The Problem String: MEYGGZRYGQYGMZRRMNRDCYJVGYZDQY3CGU4TEYJSGQ2TKM3EGMZUYOCMKE2EESSLIJBVQQSNHAYVEUZYGNJVKQKPK5AVKOBZJJJEKM2RJFCDQNRXHFMQ====
  • Minimal reproducable code:
<?php
require_once __DIR__ . '/vendor/autoload.php';

use PragmaRX\Google2FA\Google2FA;

$secret = 'MEYGGZRYGQYGMZRRMNRDCYJVGYZDQY3CGU4TEYJSGQ2TKM3EGMZUYOCMKE2EESSLIJBVQQSNHAYVEUZYGNJVKQKPK5AVKOBZJJJEKM2RJFCDQNRXHFMQ====';
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // As used by PragmaRX\Google2FA\Support\Constants::ALPHABET
$code = '123456';

echo "<h3>Direct Preg Match Test (Simulating Library Logic)</h3>\n";
$secretWithoutPadding = preg_replace('/=+$/', '', $secret);
$pattern = '/^[' . $alphabet . ']+$/'; // This pattern is derived from the library's Base32::checkForValidCharacters method
$isMatch = preg_match($pattern, $secretWithoutPadding);
echo "preg_match result: "; var_dump($isMatch); echo "\n";
if ($isMatch === 1) {
    echo "Direct preg_match: SUCCESSFUL MATCH.\n";
} else {
    echo "Direct preg_match: FAILED TO MATCH (preg_last_error: " . preg_last_error() . " - " . preg_last_error_msg() . ").\n";
}
echo "\n";

echo "<h3>Google2FA Library Test</h3>\n";
$google2fa = new Google2FA();
try {
    echo "Attempting \$google2fa->verifyKey(\$secret, \$code)...\n";
    $valid = $google2fa->verifyKey($secret, $code);
    echo "verifyKey result: "; var_dump($valid); echo "\n";
} catch (\Exception $e) {
    echo "Exception caught from verifyKey:\n";
    echo "Message: " . $e->getMessage() . "\n";
    echo "File: " . $e->getFile() . " :: Line " . $e->getLine() . "\n";
    // Optionally include first few lines of trace if concise
    echo "Trace:\n" . $e->getTraceAsString() . "\n";
}
?>
  • Actual output of script:
<h3>Direct Preg Match Test</h3>
preg_match result: int(1)

Direct preg_match: SUCCESSFUL MATCH.

<h3>Google2FA Library Test</h3>
Attempting $google2fa->verifyKey($secret, $code)...
Exception caught from verifyKey:
Message: Invalid characters in the base32 string.
File: /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php :: Line 205
Trace:
#0 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php(164): PragmaRX\Google2FA\Google2FA->checkForValidCharacters()
#1 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php(75): PragmaRX\Google2FA\Google2FA->validateSecret()
#2 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(284): PragmaRX\Google2FA\Google2FA->base32Decode()
#3 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(82): PragmaRX\Google2FA\Google2FA->oathTotp()
#4 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(472): PragmaRX\Google2FA\Google2FA->findValidOTP()
#5 /home/vagrant/code/ccbv5/app/Http/Controllers/TestController.php(138): PragmaRX\Google2FA\Google2FA->verifyKey()
...

The expected output would be: verifyKey should process the secret without throwing an InvalidCharactersException, and then return false (because 123456 is a dummy code).

I've run this code both inside and outside of my Laravel framework and receive the same error message, so it doesn't seem to be some kind of conflict with a constant being overwritten.

If I generate a secret directly with $google2fa->generateSecretKey() and then run $google2fa->verifyKey() with that secret, it works exactly as expected. It's just an issue with valid base32 codes that were generated using the old code. Obviously I can't switch to this project if I'm unable to have the old codes work.

Any ideas what may be causing this or any additional information you need from me?

Originally created by @brandonobrien on GitHub (May 9, 2025). Original GitHub issue: https://github.com/antonioribeiro/google2fa/issues/226 I'm trying to migrate from `sonata-project/google-authenticator` since it's been abandoned for a while. I have thousands of 2fa codes set up that are based on an md5 hash + 2fa salt environment value. These are then converted to base32 and used for creating the QR code as well as validating the 6 digit pin. The issue I'm running into is `verifyKey` is throwing an `InvalidCharactersException` even though the string is valid. I wrote a test script and verified the string is valid through several different means. I'll include as much information as I can, including the test script I wrote, that proves this issue. * Library Version: `pragmarx/google2fa v8.0.3` (added as `"pragmarx/google2fa": "^8.0"` in my `composer.json` file * PHP Version: `8.3.1` * The Problem String: `MEYGGZRYGQYGMZRRMNRDCYJVGYZDQY3CGU4TEYJSGQ2TKM3EGMZUYOCMKE2EESSLIJBVQQSNHAYVEUZYGNJVKQKPK5AVKOBZJJJEKM2RJFCDQNRXHFMQ====` * Minimal reproducable code: ``` <?php require_once __DIR__ . '/vendor/autoload.php'; use PragmaRX\Google2FA\Google2FA; $secret = 'MEYGGZRYGQYGMZRRMNRDCYJVGYZDQY3CGU4TEYJSGQ2TKM3EGMZUYOCMKE2EESSLIJBVQQSNHAYVEUZYGNJVKQKPK5AVKOBZJJJEKM2RJFCDQNRXHFMQ===='; $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // As used by PragmaRX\Google2FA\Support\Constants::ALPHABET $code = '123456'; echo "<h3>Direct Preg Match Test (Simulating Library Logic)</h3>\n"; $secretWithoutPadding = preg_replace('/=+$/', '', $secret); $pattern = '/^[' . $alphabet . ']+$/'; // This pattern is derived from the library's Base32::checkForValidCharacters method $isMatch = preg_match($pattern, $secretWithoutPadding); echo "preg_match result: "; var_dump($isMatch); echo "\n"; if ($isMatch === 1) { echo "Direct preg_match: SUCCESSFUL MATCH.\n"; } else { echo "Direct preg_match: FAILED TO MATCH (preg_last_error: " . preg_last_error() . " - " . preg_last_error_msg() . ").\n"; } echo "\n"; echo "<h3>Google2FA Library Test</h3>\n"; $google2fa = new Google2FA(); try { echo "Attempting \$google2fa->verifyKey(\$secret, \$code)...\n"; $valid = $google2fa->verifyKey($secret, $code); echo "verifyKey result: "; var_dump($valid); echo "\n"; } catch (\Exception $e) { echo "Exception caught from verifyKey:\n"; echo "Message: " . $e->getMessage() . "\n"; echo "File: " . $e->getFile() . " :: Line " . $e->getLine() . "\n"; // Optionally include first few lines of trace if concise echo "Trace:\n" . $e->getTraceAsString() . "\n"; } ?> ``` * Actual output of script: ``` <h3>Direct Preg Match Test</h3> preg_match result: int(1) Direct preg_match: SUCCESSFUL MATCH. <h3>Google2FA Library Test</h3> Attempting $google2fa->verifyKey($secret, $code)... Exception caught from verifyKey: Message: Invalid characters in the base32 string. File: /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php :: Line 205 Trace: #0 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php(164): PragmaRX\Google2FA\Google2FA->checkForValidCharacters() #1 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Support/Base32.php(75): PragmaRX\Google2FA\Google2FA->validateSecret() #2 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(284): PragmaRX\Google2FA\Google2FA->base32Decode() #3 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(82): PragmaRX\Google2FA\Google2FA->oathTotp() #4 /home/vagrant/code/ccbv5/vendor/pragmarx/google2fa/src/Google2FA.php(472): PragmaRX\Google2FA\Google2FA->findValidOTP() #5 /home/vagrant/code/ccbv5/app/Http/Controllers/TestController.php(138): PragmaRX\Google2FA\Google2FA->verifyKey() ... ``` The expected output would be: `verifyKey` should process the secret without throwing an `InvalidCharactersException`, and then return false (because 123456 is a dummy code). I've run this code both inside and outside of my Laravel framework and receive the same error message, so it doesn't seem to be some kind of conflict with a constant being overwritten. If I generate a secret directly with `$google2fa->generateSecretKey()` and then run `$google2fa->verifyKey()` with that secret, it works exactly as expected. It's just an issue with valid base32 codes that were generated using the old code. Obviously I can't switch to this project if I'm unable to have the old codes work. Any ideas what may be causing this or any additional information you need from me?
Author
Owner

@rimas-kudelis commented on GitHub (May 27, 2025):

@brandonobrien , you're passing $secret to the validator, but you're testing your assumptions with $secretWithoutPadding. I believe padding is exactly what causes the issue, because if you look at the code of this library, it never removes any padding, and = is not an acceptable character for base32.

And if you drop padding, this key will still fail due to unacceptable key length. You might want to look into generating keys that don't have these issues at least in future (and maybe migrating affected users to "valid" keys as well).

I'm in the same boat as you, tasked with a migration from sonata-project/google-authenticator, but if you look at the code of that library, it's both very minimalistic and yet surprisingly clean. If you want to keep your current 2FA keys, you might consider retaining that dependency on the abandoned package, or looking at its forks/compatible packages (there are some, but the ones I checked were hardly any better code-wise), or even adopting its code as classes in your own project. However, you pay some attention to the fact that that library generates links to a third-party service when asked to generate a QR code URL. If you're using that functionality, you're essentially trusting that third party with all TOTPs of your users (as well as sending their usernames to it). I'm replacing that bit with local generation via endroid/qr-code[-bundle] and returning data URIs of these generated images instead,

<!-- gh-comment-id:2912418939 --> @rimas-kudelis commented on GitHub (May 27, 2025): @brandonobrien , you're passing `$secret` to the validator, but you're testing your assumptions with `$secretWithoutPadding`. I believe padding is exactly what causes the issue, because if you look at the code of this library, it never removes any padding, and `=` is not an acceptable character for base32. And if you drop padding, this key will still fail due to unacceptable key length. You might want to look into generating keys that don't have these issues at least in future (and maybe migrating affected users to "valid" keys as well). I'm in the same boat as you, tasked with a migration from `sonata-project/google-authenticator`, but if you look at the code of that library, it's both very minimalistic and yet surprisingly clean. If you want to keep your current 2FA keys, you might consider retaining that dependency on the abandoned package, or looking at its forks/compatible packages (there are some, but the ones I checked were hardly any better code-wise), or even adopting its code as classes in your own project. However, you pay some attention to the fact that that library generates links to a third-party service when asked to generate a QR code URL. If you're using that functionality, you're essentially trusting that third party with all TOTPs of your users (as well as sending their usernames to it). I'm replacing that bit with local generation via `endroid/qr-code[-bundle]` and returning data URIs of these generated images instead,
Sign in to join this conversation.
No labels
bug
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/google2fa#564
No description provided.