[GH-ISSUE #123] Google2FA::generateSecretKey() is not compatible with Google 2FA #292

Closed
opened 2026-03-01 17:48:57 +03:00 by kerem · 14 comments
Owner

Originally created by @hopeseekr on GitHub (Jul 14, 2019).
Original GitHub issue: https://github.com/antonioribeiro/google2fa/issues/123

The Problem

Google 2FA requires a minimum of 20 bytes for a secret key. Google2FA::generateSecretKey() only produces 16 bytes by default.

image

The Solution

Up the default to 20.

Originally created by @hopeseekr on GitHub (Jul 14, 2019). Original GitHub issue: https://github.com/antonioribeiro/google2fa/issues/123 ## The Problem Google 2FA requires a minimum of 20 bytes for a secret key. `Google2FA::generateSecretKey()` only produces 16 bytes by default. ![image](https://user-images.githubusercontent.com/1125541/61188470-1feb9d80-a645-11e9-9e5c-48d7193dd1d1.png) ## The Solution Up the default to 20.
kerem closed this issue 2026-03-01 17:48:58 +03:00
Author
Owner

@antonioribeiro commented on GitHub (Sep 11, 2019):

Was this somehow changed?
Because it was 16, I did this years ago and it was ever 16 bytes...

<!-- gh-comment-id:530524604 --> @antonioribeiro commented on GitHub (Sep 11, 2019): Was this somehow changed? Because it was 16, I did this years ago and it was ever 16 bytes...
Author
Owner

@raftalks commented on GitHub (Sep 11, 2019):

I have tested this and works without any issues.

<!-- gh-comment-id:530545154 --> @raftalks commented on GitHub (Sep 11, 2019): I have tested this and works without any issues.
Author
Owner

@antonioribeiro commented on GitHub (Sep 11, 2019):

I have tested this and works without any issues.

I believe it does. What I want to know is why is this even necessary since we have thousands of users using it without any trouble using 16 bytes. This was never necessary for Google 2FA.

<!-- gh-comment-id:530552599 --> @antonioribeiro commented on GitHub (Sep 11, 2019): > I have tested this and works without any issues. I believe it does. What I want to know is why is this even necessary since we have thousands of users using it without any trouble using 16 bytes. This was never necessary for Google 2FA.
Author
Owner

@antonioribeiro commented on GitHub (Sep 12, 2019):

I see now. Thank you.

<!-- gh-comment-id:530619860 --> @antonioribeiro commented on GitHub (Sep 12, 2019): I see now. Thank you.
Author
Owner

@antonioribeiro commented on GitHub (Sep 12, 2019):

Changing it made tests to break. I still cannot find the error. Could you provide some snippet showing it?

<!-- gh-comment-id:530623159 --> @antonioribeiro commented on GitHub (Sep 12, 2019): Changing it made tests to break. I still cannot find the error. Could you provide some snippet showing it?
Author
Owner

@raftalks commented on GitHub (Sep 12, 2019):

20 is not the correct length by bits, 16 then 32 then 64, so I still don't think that is the root issue reported by @hopeseekr

What I can tell you is that it will throw an exception where if the token secret was placed as the first argument in place of where the user secret must be (which I did without checking the docs) and had similar issue that brought me to this thread, but I figured it out and all works good for me.

$valid = $google2fa->verifyKey($user->google2fa_secret, $secret);
$valid = $google2fa->verifyKey($secret, $user->google2fa_secret); <--- this is wrong and throws error with compatibility issue to GoogleAuthenticator

<!-- gh-comment-id:530774287 --> @raftalks commented on GitHub (Sep 12, 2019): 20 is not the correct length by bits, 16 then 32 then 64, so I still don't think that is the root issue reported by @hopeseekr What I can tell you is that it will throw an exception where if the token secret was placed as the first argument in place of where the user secret must be (which I did without checking the docs) and had similar issue that brought me to this thread, but I figured it out and all works good for me. $valid = $google2fa->verifyKey($user->google2fa_secret, $secret); $valid = $google2fa->verifyKey($secret, $user->google2fa_secret); <--- this is wrong and throws error with compatibility issue to GoogleAuthenticator
Author
Owner

@wells commented on GitHub (Sep 18, 2019):

According to the PragmaRX\Google2FA\Support\Base32 trait, the IncompatibleWithGoogleAuthenticatorException is thrown as a result of failing the bitwise & operator test found within the checkGoogleAuthenticatorCompatibility() function.

Here's what I see:

  • The test is (strlen($b32) & (strlen($b32) - 1)) !== 0
  • For a 20 byte string, the test would be written as (20 & 19) !== 0

The & bitwise operator provides "the bits that are set in both $a and $b."

In this instance, we can see the action more easily in base 2 (i.e. binary):

// Example 1: Base 10
20 & 19 = 16

// Example 1: Base 2
  10100 
& 10011
--------
  10000

// Example 2: Base 10
16 & 15 = 0

// Example 2: Base 2
  10000
& 01111
--------
  00000

// Other examples
8 & 7 = 0
...
16 & 15 = 0
17 & 16 = 16
18 & 17 = 16
19 & 18 = 18
20 & 19 = 16
21 & 20 = 20
22 & 21 = 20
23 & 22 = 22
24 & 23 = 16
25 & 24 = 24
26 & 25 = 24
27 & 26 = 26
28 & 27 = 24
29 & 28 = 28
30 & 29 = 28
31 & 30 = 30
32 & 31 = 0
...
64 & 63 = 0
...
128 & 127 = 0
<!-- gh-comment-id:532820384 --> @wells commented on GitHub (Sep 18, 2019): According to the `PragmaRX\Google2FA\Support\Base32` trait, the `IncompatibleWithGoogleAuthenticatorException` is thrown as a result of failing the bitwise `&` operator test found within the `checkGoogleAuthenticatorCompatibility()` function. Here's what I see: - The test is `(strlen($b32) & (strlen($b32) - 1)) !== 0` - For a 20 byte string, the test would be written as `(20 & 19) !== 0` The `&` bitwise operator provides "the bits that are set in both $a and $b." In this instance, we can see the action more easily in base 2 (i.e. binary): ```php // Example 1: Base 10 20 & 19 = 16 // Example 1: Base 2 10100 & 10011 -------- 10000 // Example 2: Base 10 16 & 15 = 0 // Example 2: Base 2 10000 & 01111 -------- 00000 // Other examples 8 & 7 = 0 ... 16 & 15 = 0 17 & 16 = 16 18 & 17 = 16 19 & 18 = 18 20 & 19 = 16 21 & 20 = 20 22 & 21 = 20 23 & 22 = 22 24 & 23 = 16 25 & 24 = 24 26 & 25 = 24 27 & 26 = 26 28 & 27 = 24 29 & 28 = 28 30 & 29 = 28 31 & 30 = 30 32 & 31 = 0 ... 64 & 63 = 0 ... 128 & 127 = 0 ```
Author
Owner

@antonioribeiro commented on GitHub (Sep 18, 2019):

Let me try to explain this better:

1) You can use whatever shared secret you want.

You don't really need to use the generateSecretKey(), this is just a helper.

You can generate it yourself, as long as it is converted to a base32 string when you give it to Google Authenticator. Usually base32 strings are generated by converting a base256 string to a base32 one.

2) I prefer 20 chars to generate keys because

20 chars in base256 === 32 chars in base32 === 256 bits of a strong shared secret

Base 256 is the whole ASCII charset.

You can use as few as 10 base256 chars (16 base32 chars === 128 bits), which is the minimum.

The recommended is 160 bits (20 base32 chars === 12 base256 chars), which is the minimum.

3) But... there's a catch here, Google Authenticator will not let you use 160 bits

Because this is a 12 chars length string in base32 and it MUST be a power of 2 string length.

So the minimum (above the recommended) is 256 bits (32 chars in base32).

Again: Google Authenticator needs it to be power of 2 chars: 16, 32, 64, 128 chars, as stated by @raftalks

Which brings us to

4) The method checkGoogleAuthenticatorCompatibility is only checking for power of 2 sized strings:

(strlen($b32) & (strlen($b32) - 1)) !== 0

Your examples, @wells, shows exactly this behaviour:

 // Other examples
8 & 7 = 0 =============== power of 2
...
16 & 15 = 0 =============== power of 2
17 & 16 = 16
18 & 17 = 16
19 & 18 = 18
20 & 19 = 16
21 & 20 = 20
22 & 21 = 20
23 & 22 = 22
24 & 23 = 16
25 & 24 = 24
26 & 25 = 24
27 & 26 = 26
28 & 27 = 24
29 & 28 = 28
30 & 29 = 28
31 & 30 = 30
32 & 31 = 0 =============== power of 2
...
64 & 63 = 0 =============== power of 2
...
128 & 127 = 0
<!-- gh-comment-id:532867720 --> @antonioribeiro commented on GitHub (Sep 18, 2019): Let me try to explain this better: #### 1) You can use whatever shared secret you want. You don't really need to use the `generateSecretKey()`, this is just a helper. You can generate it yourself, as long as it is converted to a base32 string when you give it to Google Authenticator. Usually base32 strings are generated by converting a base256 string to a base32 one. #### 2) I prefer 20 chars to generate keys because 20 chars in base256 **===** 32 chars in base32 **===** 256 bits of a strong shared secret Base 256 is the whole ASCII charset. You can use as few as 10 base256 chars (16 base32 chars === 128 bits), which is the minimum. The recommended is 160 bits (20 base32 chars === 12 base256 chars), which is the minimum. #### 3) But... there's a catch here, Google Authenticator will not let you use 160 bits Because this is a 12 chars length string in base32 and it MUST be a power of 2 string length. So the minimum (above the recommended) is 256 bits (32 chars in base32). Again: Google Authenticator needs it to be power of 2 chars: **16, 32, 64, 128 chars**, as stated by @raftalks Which brings us to #### 4) The method `checkGoogleAuthenticatorCompatibility` is only [checking for power of 2 sized strings](https://stackoverflow.com/questions/4965301/finding-if-a-number-is-a-power-of-2): ``` (strlen($b32) & (strlen($b32) - 1)) !== 0 ``` Your examples, @wells, shows exactly this behaviour: ``` // Other examples 8 & 7 = 0 =============== power of 2 ... 16 & 15 = 0 =============== power of 2 17 & 16 = 16 18 & 17 = 16 19 & 18 = 18 20 & 19 = 16 21 & 20 = 20 22 & 21 = 20 23 & 22 = 22 24 & 23 = 16 25 & 24 = 24 26 & 25 = 24 27 & 26 = 26 28 & 27 = 24 29 & 28 = 28 30 & 29 = 28 31 & 30 = 30 32 & 31 = 0 =============== power of 2 ... 64 & 63 = 0 =============== power of 2 ... 128 & 127 = 0 ```
Author
Owner

@antonioribeiro commented on GitHub (Sep 18, 2019):

I'm changing it to be more readable:

/**
 * Check if the secret key is compatible with Google Authenticator.
 *
 * @param $b32
 *
 * @throws IncompatibleWithGoogleAuthenticatorException
 */
protected function checkGoogleAuthenticatorCompatibility($b32)
{
    if (
        $this->enforceGoogleAuthenticatorCompatibility &&
        (
            $this->isCharCountNotAPowerOfTwo($b32) || // Google Authenticator requires a power of 2 base32 length string
            $this->charCountBits($b32) < 128 // minimum number of bits = 128 / recommended = 160 / compatible with GA = 256
        )
     ) {
        throw new IncompatibleWithGoogleAuthenticatorException();
    }
}
<!-- gh-comment-id:532875292 --> @antonioribeiro commented on GitHub (Sep 18, 2019): I'm changing it to be more readable: ``` php /** * Check if the secret key is compatible with Google Authenticator. * * @param $b32 * * @throws IncompatibleWithGoogleAuthenticatorException */ protected function checkGoogleAuthenticatorCompatibility($b32) { if ( $this->enforceGoogleAuthenticatorCompatibility && ( $this->isCharCountNotAPowerOfTwo($b32) || // Google Authenticator requires a power of 2 base32 length string $this->charCountBits($b32) < 128 // minimum number of bits = 128 / recommended = 160 / compatible with GA = 256 ) ) { throw new IncompatibleWithGoogleAuthenticatorException(); } } ```
Author
Owner

@wells commented on GitHub (Sep 18, 2019):

This has sent me down the RFC rabbit hole this afternoon.

RFC 4226 (https://tools.ietf.org/html/rfc4226) states the following:

R6 - The algorithm MUST use a strong shared secret.  The length of
the shared secret MUST be at least 128 bits.  This document
RECOMMENDs a shared secret length of 160 bits.

128 bits == 10 chars (base 256) == 16 chars (base 32)
160 bits == 12 chars (base 256) == 20 chars (base 32)

@antonioribeiro I'm having trouble finding the power of 2 requirement for secret length. Would you have some documentation from Google on this?

Based on what I found in the Google Authenticator wiki, it looks like this package could potentially support sha256 and sha512 hashing algorithms in addition to sha1 (used in oathHotp() function).

I also performed a cursory source dive of the iOS Google Authenticator and I'd say these algorithms may be available now. This would require an update to also include the algorithm parameter in the getQRCodeUrl() function.

https://github.com/google/google-authenticator/wiki/Key-Uri-Format#algorithm

https://github.com/google/google-authenticator/blob/master/mobile/ios/Classes/OTPGenerator.m#L105-L113

@antonioribeiro Do you know if these hashing algorithms are actually supported or is it still just SHA1?

<!-- gh-comment-id:532877115 --> @wells commented on GitHub (Sep 18, 2019): This has sent me down the RFC rabbit hole this afternoon. RFC 4226 (https://tools.ietf.org/html/rfc4226) states the following: ``` R6 - The algorithm MUST use a strong shared secret. The length of the shared secret MUST be at least 128 bits. This document RECOMMENDs a shared secret length of 160 bits. ``` `128 bits == 10 chars (base 256) == 16 chars (base 32)` `160 bits == 12 chars (base 256) == 20 chars (base 32)` @antonioribeiro I'm having trouble finding the power of 2 requirement for secret length. Would you have some documentation from Google on this? Based on what I found in the Google Authenticator wiki, it looks like this package could potentially support sha256 and sha512 hashing algorithms in addition to sha1 (used in `oathHotp()` function). I also performed a cursory source dive of the iOS Google Authenticator and I'd say these algorithms may be available now. This would require an update to also include the algorithm parameter in the `getQRCodeUrl()` function. https://github.com/google/google-authenticator/wiki/Key-Uri-Format#algorithm https://github.com/google/google-authenticator/blob/master/mobile/ios/Classes/OTPGenerator.m#L105-L113 @antonioribeiro Do you know if these hashing algorithms are actually supported or is it still just SHA1?
Author
Owner

@antonioribeiro commented on GitHub (Sep 18, 2019):

@wells, I could not find it too, but the need for a power of two string is something I checked when I first developed Google2FA, but now, to be sure, I had to do it all over again, so I used a different tool to be sure mines were ok:

https://rootprojects.org/authenticator/

Tested all of those one by one:

BMBCWU4VKTN3WHVG - 16 chars - 128 bits 1
UIRGD7M5Y7BFTRQWV - 17 chars - 136 bits
AUZSAZZP6VK2KL3WUU - 18 chars - 144 bits
J4TKEMKICE2GBBNO2WY - 19 chars - 152 bits
EU734YCWH6RKV7RFGMS7 - 20 chars - 160 bits
GSJUEBTM4SSBAYFR2YJD3 - 21 chars - 168 bits
ZQX5T5375AOBXJASK2O6BS - 22 chars - 176 bits
VBUQKWB47FGWRSQBFJZJ5SK - 23 chars - 184 bits

4REXZV2S7PTTE6LUWVY7XSBWGUCML37T - 32 chars - 256 bits 1

TWU3QWJJQ3PI4XZWD5RREIRIAKIXF5FG5DD2IV6LCBS4EV5RNDUVDDXAORX336K4 - 64 chars - 512 bits 1

RCOZMBSOY6QPP7BBSZUYVYRKSNUNKXOFW3KCGA3GEGNA3TV2Z6XYIEMNRNTJDPWK7S2BYUJ4UBD4MOLAJH5MNURWHG45HXBL3Y37ZWT7IDIFR5J4LURAJQMGILUZRNW7 - 128 chars - 1024 bits 1
<!-- gh-comment-id:532882122 --> @antonioribeiro commented on GitHub (Sep 18, 2019): @wells, I could not find it too, but the need for a power of two string is something I checked when I first developed Google2FA, but now, to be sure, I had to do it all over again, so I used a different tool to be sure mines were ok: https://rootprojects.org/authenticator/ Tested all of those one by one: ``` BMBCWU4VKTN3WHVG - 16 chars - 128 bits 1 UIRGD7M5Y7BFTRQWV - 17 chars - 136 bits AUZSAZZP6VK2KL3WUU - 18 chars - 144 bits J4TKEMKICE2GBBNO2WY - 19 chars - 152 bits EU734YCWH6RKV7RFGMS7 - 20 chars - 160 bits GSJUEBTM4SSBAYFR2YJD3 - 21 chars - 168 bits ZQX5T5375AOBXJASK2O6BS - 22 chars - 176 bits VBUQKWB47FGWRSQBFJZJ5SK - 23 chars - 184 bits 4REXZV2S7PTTE6LUWVY7XSBWGUCML37T - 32 chars - 256 bits 1 TWU3QWJJQ3PI4XZWD5RREIRIAKIXF5FG5DD2IV6LCBS4EV5RNDUVDDXAORX336K4 - 64 chars - 512 bits 1 RCOZMBSOY6QPP7BBSZUYVYRKSNUNKXOFW3KCGA3GEGNA3TV2Z6XYIEMNRNTJDPWK7S2BYUJ4UBD4MOLAJH5MNURWHG45HXBL3Y37ZWT7IDIFR5J4LURAJQMGILUZRNW7 - 128 chars - 1024 bits 1 ```
Author
Owner

@antonioribeiro commented on GitHub (Sep 18, 2019):

This is it with 20 chars (160 bits):

image

<!-- gh-comment-id:532882850 --> @antonioribeiro commented on GitHub (Sep 18, 2019): This is it with 20 chars (160 bits): ![image](https://user-images.githubusercontent.com/3182864/65188948-fa648100-da45-11e9-899e-38e92ad46877.png)
Author
Owner

@wells commented on GitHub (Sep 18, 2019):

Powers of 2 is it then.

<!-- gh-comment-id:532883875 --> @wells commented on GitHub (Sep 18, 2019): Powers of 2 is it then.
Author
Owner

@antonioribeiro commented on GitHub (Sep 19, 2019):

Are we all good with a this point? @hopeseekr , are you still following your thread, mate?

<!-- gh-comment-id:533264471 --> @antonioribeiro commented on GitHub (Sep 19, 2019): Are we all good with a this point? @hopeseekr , are you still following your thread, mate?
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#292
No description provided.