Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OTP Generated by Google Authenticator not validated by the Library's OTP #94

Open
Cprime50 opened this issue Jul 2, 2024 · 40 comments · May be fixed by #99
Open

OTP Generated by Google Authenticator not validated by the Library's OTP #94

Cprime50 opened this issue Jul 2, 2024 · 40 comments · May be fixed by #99

Comments

@Cprime50
Copy link

Cprime50 commented Jul 2, 2024

I am using this library to generate a QR code that I connect to Google Authenticator. However, the OTP generated by Google Authenticator doesn't get validated by the library.

version of package

github.com/pquerna/otp v1.4.0

go version

go version go1.22.4 linux/amd64

code:

package main

import (
	"fmt"
	"image/png"
	"log"
	"os"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
)

func main() {
	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      "localhost:8000",
		AccountName: "[email protected]",
		SecretSize:  32,
	})
	if err != nil {
		log.Fatalf("Failed to generate OTP: %v", err)
	}

	qrImage, err := key.Image(256, 256)
	if err != nil {
		log.Fatalf("Failed to generate QR code image: %v", err)
	}
	file, err := os.Create("otp_qrcode.png")
	if err != nil {
		log.Fatalf("Failed to create file for QR code: %v", err)
	}
	defer file.Close()
	if err := png.Encode(file, qrImage); err != nil {
		log.Fatalf("Failed to save QR code image: %v", err)
	}
	fmt.Println("QR code generated and saved as otp_qrcode.png")

	fmt.Print("Enter the OTP code from your authenticator app: ")
	var otpCode string
	fmt.Scanln(&otpCode)

	valid := totp.Validate(otpCode, key.Secret())
	if valid {
		fmt.Println("OTP verified successfully!")
	} else {
		fmt.Println("Invalid OTP code.")
	}
}

to reproduce:
Run the code provided above.
Scan the generated QR code using Google Authenticator.
Enter the OTP code displayed in Google Authenticator into the terminal.

@KEINOS
Copy link

KEINOS commented Aug 25, 2024

I can not reproduce in my environment and all the authenticator apps below returns as success, "OTP verified successfully!"

  • Authenticators
    • Android 9 (Kernel 4.9.148)
      • Google Authenticator 6.0 ... OK
      • Microsoft Authenticator 6.2407.5108 ... OK
    • iPhone (iOS 17.6.1)
      • Google Authenticator 4.2.1 ... OK
      • Microsoft Authenticator 6.8.14 ... OK
  • Code executed via macOS, Linux (Alpine, Ubuntu via Docker) @ go1.23.0
  • Package: github.com/pquerna/otp v1.4.0

In my experience, the time zone and/or NTP are not set correctly in such cases.

@the-real-i9
Copy link

the-real-i9 commented Sep 7, 2024

Hi, @Cprime50. I reproduced your code and it worked for me.

Go version (Latest)

go version go1.23.0 linux/amd64

I don't think this issue has anything to do with Go's version.

Package version

github.com/pquerna/otp v1.4.0

@yogo1212
Copy link

yogo1212 commented Oct 21, 2024

I've found that sometimes the secret parameter is pushed to the end of the query. Then, the Google Authenticator tells me to use the camera. The ios camera app adds the TOTP params to the keystore and produces working codes.

While it is funny that Google asks me to use Apple software because they suck, maybe there's good reason for this package to make sure the secret is the first parameter in the query.

@yogo1212
Copy link

yogo1212 commented Oct 21, 2024

package main

import (
	"crypto/rand"
	"log"
	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
)

func main() {
	secret := make([]byte, 20)
	rand.Read(secret)

	t, err := totp.Generate(totp.GenerateOpts{
		"issuer",
		"[email protected]",
		45,
		uint(len(secret)),
		secret,
		otp.DigitsEight,
		otp.AlgorithmSHA256,
		rand.Reader,
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Println(t.URL())
}

secret at the end -> gauth throws in the towel

@yogo1212
Copy link

https://github.com/pquerna/otp/blob/master/internal/encode.go#L21

this is the culprit.
can't sort the keys.

@yogo1212
Copy link

i'll create a PR

@KEINOS
Copy link

KEINOS commented Nov 16, 2024

According to the below specs, "secret" is the only required parameter and all the examples begins with "secret" parameter.

Due to the random nature of Golang maps, "secret" does not appear statically in the first parameter. Therefore, I was almost convinced that GoogleAuthenticator had a rule that the first parameter had to be "secret".

But on the other hand, the specifications do not stipulate that the first parameter must be "secret".

I have looked at the old source code of GoogleAuthenticator for both Android and iOS and it seems that neither of them check/restricts the order of the parameters in the OTP URI.

So even if it does no harm if the first parameter is forced to be "secret", certain precautions should be taken.

Because I suspect that there is a deeper reason for this.

I created the below test code. It creates QR code images for both URI with "secret" parameter at the head and the tail. And they both worked fine on my Android and iPhone authenticators (Google Authenticator and Microsoft Authenticator).

@yogo1212 Does the blow reproduce the problem?

TEST CODE
package main

import (
	"image"
	"image/png"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	const (
		// valid URI which begins with a secret parameter
		validURI = "otpauth://totp/localhost:8000:[email protected]?" +
			"secret=3GTPYMOZFQGN26SXJRB3RUHFR4NESG4PRNFX2PBGRDBGKRYNRE4A&algorithm=SHA1&digits=6&issuer=localhost:8000&period=30"
		// invalid URI which ends with a secret parameter
		invalidURI = "otpauth://totp/localhost:8000:[email protected]?" +
			"algorithm=SHA1&digits=6&issuer=localhost:8000&period=30&secret=3GTPYMOZFQGN26SXJRB3RUHFR4NESG4PRNFX2PBGRDBGKRYNRE4A"
	)

	// generate QR code for valid URI and save it to file
	okImage, err := getQR(validURI, 256, 256)
	panicOnError(err)

	err = saveImg(okImage, "valid.png")
	panicOnError(err)

	// generate QR code for invalid URI and save it to file
	ngImage, err := getQR(invalidURI, 256, 256)
	panicOnError(err)

	err = saveImg(ngImage, "invalid.png")
	panicOnError(err)
}

func saveImg(img image.Image, filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	if err := png.Encode(file, img); err != nil {
		return err
	}

	return nil
}

func getQR(orig string, width int, height int) (image.Image, error) {
	b, err := qr.Encode(orig, qr.M, qr.Auto)
	if err != nil {
		return nil, err
	}

	b, err = barcode.Scale(b, width, height)
	if err != nil {
		return nil, err
	}

	return b, nil
}

func panicOnError(err error) {
	if err != nil {
		panic(err)
	}
}

@yogo1212
Copy link

yogo1212 commented Nov 16, 2024

@KEINOS there's a draft RFC that (seemingly) didn't go through (err.. it expired. where is my mind).
(tracking page for that rfc)
until there's standardisation, we have to go by what works with the software in the field. sadly.

the rfc links to a blog post exploring the situation. google worked on the thing until they were happy with out, producing a de-facto-but-not-really standard, and left the cleanup for the rest of the world.

i created #95 in the meantime.

@KEINOS
Copy link

KEINOS commented Nov 19, 2024

@yogo1212

That's very informative! Thank you.

I am so tired of Google getting bored halfway through designing something - and then expecting the rest of us to just figure out what they meant.

Indeed, the blog explains everything.

until there's standardisation, we have to go by what works with the software in the field. sadly.

I do understand and agree.

But what I'm pointing out here is that I can not reproduce the problem on any device and AuthApps that I have.
Even forcing the last parameter be the "secret".

So, I have a hunch that the order of the parameters itself is not the cause of the problem.

However, let's assume that in the case of GoogleAuthenticator, the first parameter must be "secret" and is mandatory.

If this is due to the order of the parameters, it will lead to conflicts in the future if other providers choose different specifications. As noted in the blog, different providers have different specifications.

To continue the discussion for closing, shouldn't the order of the optional parameters be configurable as follows?
This should ensure backward compatibility of the package as well.

- func EncodeQuery(v url.Values) string {...}
+ func EncodeQuery(v url.Values, paramKeys ...string) string {
+     if len(paramKeys) == 0 {
+         return ...(original behaviour)...
+     }
+
+     for _, paramKey := range paramKeys {
+         // append to the URI if paramKey exists in v
+     }
+     return ...(custom ordered)...
+ }

@yogo1212
Copy link

yogo1212 commented Nov 19, 2024

@KEINOS most authenticators don't impose any particular order - which is the sane thing to do.
query params aren't usually ordered anyway (maybe except for when multiple asdf[]= params are being used to construct a list). they are primarily a key-value mapping.

since we aren't aware of any other restrictions when it comes to ordering params, the solution you suggested is probably overkill.
ideally, google invests the 5 minutes to make their app more relaxed (they also don't have any standard that they can't point to to justify their behaviour).

then, the workaround could even be removed from here.

@yogo1212
Copy link

yogo1212 commented Nov 19, 2024

since google, apple, etc. brought us into this age, i see little reason against creating a negative review on any of the app stores, explaining this problem.

in fact, i got my hands on a relative's iphone just to do that.

This app doesn't work when the secret isn't the first parameter in the QR code. There's no reason why it shouldn't.

for those of you want to their spend their time being annoyed by this issue more productively, i can only recommend doing the same.

@KEINOS
Copy link

KEINOS commented Nov 19, 2024

I agree that my propose may be a bit over the top.

in fact, i got my hands on a relative's iphone just to do that.

Can you provide us the URI and the QR code of that particular example?

I'm not trying to be offensive, just looking for a reproducible example. So we can find the actual problem and make this package handle one thing and well.

I like this package and believe that it will make my application more secure.
Thus, I want to find the cause rather than "It works, so be it" way, you know.

@yogo1212
Copy link

@KEINOS

doesn't work (QR code):

otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X

does work:

otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45

this is for the cross-checking the generated otp code:

oathtool --totp=SHA256 --digits=8 -s 45s -b DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X

@yogo1212
Copy link

█████████████████████████████████████████████████
█████████████████████████████████████████████████
████ ▄▄▄▄▄ █▄ █▀ ████▀▀▄▄ ▀ █▀▄█ █ ▄▄█ ▄▄▄▄▄ ████
████ █   █ █  ▄▄▀▄  ██▀ ▀ █ ▄ ▀▀█▀█ ▄█ █   █ ████
████ █▄▄▄█ █▄▀█▄██ █▄▄ ▀ ▀▀▄▄██▄█▄▄  █ █▄▄▄█ ████
████▄▄▄▄▄▄▄█▄█▄▀ █ █▄▀▄█▄█ █▄█▄█ ▀ █▄█▄▄▄▄▄▄▄████
████▄▄ ▄██▄█▄ ▀▀▀█▄▄█▄  ▀▄█▀▀█ █ ▄ ▀▀▄▀▀ ▄▄█ ████
████▄▄ ▀█▀▄▀▀▀  ▄ ▄█▄█▄█▄▄█ ▀  ██▄██▀  ▀ █▄ █████
████  ▀▀▄ ▄▀ ▀▄ ██▀  █▀██▀█ ▄▄  ▄▄█ ██▀███  ▀████
█████▀▄▀▄█▄▀▀█▀█ ▄▄▀██▀▄█ ▀█ ▄▄▀ ██   ▄ ▄▀▄ ▄████
████▄▄▄█  ▄ ▀   ▄ █▀ █▄▄▀   ▀▀█▀▄█▄▄ ▄█▄▄ █ █████
████▀▄ ▄▀ ▄█▄▀▄▀▀▀▀ ██▄▀▄▀▀▀██▄ █▀▀██ █▄▀ ▀▀█████
████▀██ █▄▄▄▄█▄▄█ ██▄▀ █▀██ ▀█▄▄█ ▄ ▀  ▀ ▀▄█▀████
████ █  █ ▄▀▄ █▀▀▀   ▄ ▀▄█▄▄ ▀▄█▄█▄██ ▄ █▄▄ █████
████ ▀ ▀█ ▄██▀▄▄█▄ ▀▄▄  █▀▀▄ ▄ ▀▄ ▄▀█▀▀▀█▀ █ ████
████ █▀▄▀▀▄▄█▄█▀ ▄ ▀█▀▀  ▄▄█ █▀▀▀▄▀  ███▄▀█  ████
████▄▀▄  ▄▄▀█▄ ▄▄▀▄█ ▀▀█▄▀█ ▄ ▀  ▀▄▄ ▄█ ▄▄█▄█████
████▄██▀▄ ▄▄ ▀▀▀█▄▄▀▄▀▀█ █ ▀ █ ▄▀ █▀███▄  ▀▀▀████
█████▄▄▄▄▄▄▄▀▀▀▀▄█▄  ▀▄ ▀▄█▀██▄█   █ ▄▄▄ ▀▄  ████
████ ▄▄▄▄▄ ██ ▄▄▀▀▄█▀▄█▀ █▀▄  ▄▀▄▄▀  █▄█ █▄█▀████
████ █   █ █▀ █▀ ▀▀██  ▄▄▄ ▄▀  ▀▀ ██  ▄▄ █ ▄ ████
████ █▄▄▄█ █ ██ █▄▀▄▄▄█▀█▀ █▄█ █▀▄█▄▀▄▀█▀▄▄ ▄████
████▄▄▄▄▄▄▄█▄▄██▄▄█▄█▄▄▄▄▄▄▄█▄█▄██▄█▄█▄▄▄▄█▄█████
█████████████████████████████████████████████████
█████████████████████████████████████████████████

@yogo1212
Copy link

urgh. that looks horrible in here...

@KEINOS
Copy link

KEINOS commented Nov 20, 2024

urgh. that looks horrible in here...

Indeed, the camera didn't recognize the QR code. 😄
Can you provide the PNG image rather than the CLI output one?

I'll write a test code with the TOTP URIs given and compare the image information.

P.S.
By the way, I noticed that Microsoft Authenticator (on iPhone) does not work for 8 digit TOTP URIs. It enforces to use 6 digit. But let's stick with GoogleAuthenticator for now.

@KEINOS
Copy link

KEINOS commented Nov 20, 2024

@yogo1212

I made a simple test code. Here are the produced images. Can you read them?

  • QR Code image of invalid URI
    invalid
    Hash of the image (SHA1): f465dc17859116846f78af62baaf81bbac408a16

  • QR Code image of valid URI
    valid
    Hash of the image (SHA1): 3b5df753bd15e9a848e7efec267c8096605652cd

Test Code
  • main.go
package main

import (
	"crypto/sha1"
	"fmt"
	"image"
	"image/png"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
)

func main() {
	const (
		// invalid URI which ends with a secret parameter
		invalidURI = "otpauth://totp/domain.com:[email protected]?" +
			"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"
		// valid URI which begins with a secret parameter
		validURI = "otpauth://totp/domain.com:[email protected]?" +
			"secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45"
		// Image size of the QR code
		imgSize = 256
	)

	// Create a new key from the URI
	keyInvalid, err := otp.NewKeyFromURL(invalidURI) // Invalid case
	panicOnError(err)

	keyValid, err := otp.NewKeyFromURL(validURI) // Valid case
	panicOnError(err)

	// Generate QR code images
	imgInvalid, err := keyInvalid.Image(imgSize, imgSize)
	panicOnError(err)

	imgValid, err := keyValid.Image(imgSize, imgSize)
	panicOnError(err)

	// Save the QR code to a file
	panicOnError(saveImg(imgInvalid, "invalid.png"))
	panicOnError(saveImg(imgValid, "valid.png"))

	// Print the SHA1 hash of the QR code images for debugging
	fmt.Println("QR code images saved. (Below are the SHA1 hash of the images)")
	panicOnError(printFileHash("invalid.png"))
	panicOnError(printFileHash("valid.png"))

	fmt.Println("\n" + "Open the QR code images and scan it with the Authenticator app to register.")
	fmt.Println("- Invalid QR code img: invalid.png")
	fmt.Println("- Valid QR code img  : valid.png")

	// Watch cancel signal
	done := watchCancel()

	defer func() {
		fmt.Println("\nCancelled")
	}()

	// Print the current pass code on every second
	fmt.Println("\n" + "Current expected pass-code (Press Ctrl+C to cancel)")

	for {
		select {
		case <-done:
			return // monitor cancelled
		default:
			timeNow := time.Now()

			// Get the current pass code
			passInvalid, err := totp.GenerateCodeCustom(
				keyInvalid.Secret(),
				timeNow,
				totp.ValidateOpts{
					Period:    uint(keyInvalid.Period()),
					Digits:    keyInvalid.Digits(),
					Algorithm: keyInvalid.Algorithm(),
				},
			)
			panicOnError(err)

			passValid, err := totp.GenerateCodeCustom(
				keyValid.Secret(),
				timeNow,
				totp.ValidateOpts{
					Period:    uint(keyValid.Period()),
					Digits:    keyValid.Digits(),
					Algorithm: keyValid.Algorithm(),
				},
			)
			panicOnError(err)

			// Print the pass code
			fmt.Print("\r- Invalid: ", passInvalid, " | Valid: ", passValid)

			time.Sleep(time.Second)
		}
	}
}

func printFileHash(pathImg string) error {
	imgData, err := os.ReadFile(pathImg)
	if err != nil {
		return err
	}

	imgHash := sha1.Sum(imgData)
	fmt.Printf("- Hash:%x (%s)\n", imgHash, pathImg)

	return nil
}

func watchCancel() chan bool {
	// Create a channel to catch signals
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	// Channel to receive signals
	done := make(chan bool, 1)

	// Spawn a goroutine to receive signals
	go func() {
		<-sigChan
		done <- true
	}()

	return done
}

func saveImg(img image.Image, filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	if err := png.Encode(file, img); err != nil {
		return err
	}

	return nil
}

func panicOnError(err error) {
	if err != nil {
		panic(err)
	}
}
  • go.mod
module example/reproduce

go 1.23.2

require github.com/boombuler/barcode v1.0.2 // indirect

require github.com/pquerna/otp v1.4.0
  • go.sum
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

P.S.
I edited the code above to use the length of the passcode as the URI definition instead of the default value(6). This change has no effect on the output QR code image.

@yogo1212
Copy link

@KEINOS yeah, ms authenticator is 💩
they won't accept the length parameter, not did find any documentation for an alternative (like there is for other app providers like twilio).
strangely, they will do 8 digits when logging into azure - which makes googling the issue a mess.

otherwise, i'm only talking about google authenticator.

see here for a comparison of providers: https://en.wikipedia.org/wiki/Comparison_of_OTP_applications

@KEINOS
Copy link

KEINOS commented Nov 22, 2024

@yogo1212

otherwise, i'm only talking about google authenticator.

Indeed. Let's stick with GoogleAuthenticator for now.

Were you able to create the PNG QR code rather than the CLI output one?
I would like to analyze the image, since if the URI is the same then the image should be the same as well.

@yogo1212
Copy link

@KEINOS qr codes can be created using different parameters, like size, fault tolerance, or encoding.
the result should always be e.g otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X.

since camera access, and qr code parsing as well, are usually implemented using OS libraries, i wouldn't expect any problems from that end.

i use qrencode -t ANSIUTF8 because it suits my workflow, there should be no difference to using https://www.qr-code-generator.com/ (although i wouldn't trust them with any secrets).

@KEINOS
Copy link

KEINOS commented Nov 23, 2024

i use qrencode -t ANSIUTF8 because it suits my workflow, there should be no difference to using https://www.qr-code-generator.com/ (although i wouldn't trust them with any secrets).

We need to isolate the problem one by one. Can you provide us that QR code image?

As you say, if the arguments and order are the same, the output image should be the same regardless of how the QR code is generated. That's why I am asking for the QR code image.

If the images differ then the problem is in the boombuler/barcode package we use. Then we can try to fix it and add a test in the CIs, comparing with the output of qrencode, for example.

The fact that the problem is not reproduced with exactly the same URI indicates that the problem may not be in the pquerna/otp package. Probably in the next stage, such as reading the QR code.

Or at least please try the test code that I provided and tell us the results?

Am I asking too much or asking some thing nonsense? > all

@KEINOS
Copy link

KEINOS commented Nov 23, 2024

I tested with the latest qrencode(v4.1.1) to generate the QR code PNG image.

But again, both invalid URI(tailing the secret param) and valid URI(heading the secret param) works fine.

It can be read by GoogleAuthenticator (iPhone, iPad and Android) without errors and generates an 8-digit passcode that is identical to pquerna/otp.

To ensure reproducibility I used Docker , but the same local version of qrencode should provide the same output. In that case simply replace "docker run --rm -i qrencode:local" to "qrencode".

#!/bin/bash

set -e

docker build --quiet -t qrencode:local .
clear;

echo "* qrencode version"
docker run --rm qrencode:local --version

invalidURI="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"
validURI="otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45"

echo; echo "* qrencode invalid URI"
echo -n "${invalidURI}" | docker run --rm -i qrencode:local -o - > invalid_qrencode.png
sha1sum invalid_qrencode.png

echo; echo "* qrencode valid URI"
echo -n "${validURI}" | docker run --rm -i qrencode:local -o - > valid_qrencode.png
sha1sum valid_qrencode.png
$ ls
Dockerfile   test.sh

$ ./test.sh
* qrencode version
qrencode version 4.1.1
Copyright (C) 2006-2017 Kentaro Fukuchi

* qrencode invalid URI
f0662befd805a31761ff2fa6df5deef78a17187d  invalid_qrencode.png

* qrencode valid URI
bcd15753a083db7aa67c6866cccb5e339cf2d543  valid_qrencode.png

$ ls
Dockerfile   test.sh  valid_qrencode.png valid_qrencode.png
Dockerfile
FROM alpine:latest AS build

ARG LIBQRENCODE_VERSION=v4.1.1

WORKDIR /root

RUN apk add --no-cache \
    alpine-sdk \
    build-base \
    cmake \
    libpng-dev

RUN \
    git clone https://github.com/fukuchi/libqrencode.git && \
    cd libqrencode && \
    git checkout -b build refs/tags/${LIBQRENCODE_VERSION} && \
    cmake BUILD_SHARED_LIBS=ON -Wno-dev . && \
    make && \
    make install

# Smoketest
RUN qrencode --version

ENTRYPOINT [ "qrencode" ]

@yogo1212
Copy link

@KEINOS this is not necessarily a question of asking "too much" :-)

to me, it was unclear whether we were talking about the same thing, which is important for the decision to divert time.
i installed google authenticator on my private device (version 7) and both qr codes produced the same (correct) code.

they also do with my "work" iphone. it has google authenticator 4.2.1. the versions don't compare because the app for ios is different from that for android.

strange.

i tried around a little and, doing nothing more than replacing the secret in your URI with one freshly generated (using the code in this comment), created this:
qr
(otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=6MIPDJVUBM7W4LOL6DNP7D4B23NM64D6)

and it doesn't work. i moved the secret to the start and it doesn't work either?!

qr
(otpauth://totp/domain.com:[email protected]?secret=6MIPDJVUBM7W4LOL6DNP7D4B23NM64D6&digits=8&issuer=domain.com&period=45&algorithm=SHA256)

so, i assume you're right about it not really being about the position of the parameter.
all of this works with the apple keychain thing.

@yogo1212
Copy link

wait a second, going back to these two:

invalidURI="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"
validURI="otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45"

the "invalid uri" does not work when generated using qrencode -t ANSIUTF8 - it does, though, when using -t PNG?!?!
is this about google authenticators inability to scan qr codes rather than about its inability to parse query params?!

but why would the result be different depending on the position of the secret?

@yogo1212
Copy link

indeed it is!

they all work with qrencode -t ANSIUTF8 -l Q 😂

looks like google does their own qr code parsing and that fails with low redundancy codes if the secret comes first (sometimes, but not always)!

@yogo1212
Copy link

wait. now the same thing works with -l L again.
this is stupid.

@yogo1212
Copy link

yogo1212 commented Nov 28, 2024

entirely stupid.

qrencode -t PNG -o /tmp/qr.png -l Q <<< 'otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X'
qr

doesn't work.

taking lines from my terminal scrollback buffer (has timestamps and comments).

this worked (when everything worked):
qrencode -t ANSIUTF8 -l Q -v 10 --verbose "$invalidURI"
but it does not work now. nor does it work with PNG.

i sent the png to a friend who has both a modern iphone and a modern android and he reports that neither code works with any of them.

@yogo1212
Copy link

everything works reliably with other apps like the rocketchat authenticator or apple keychain.

@yogo1212
Copy link

@KEINOS does your google authenticator eat that qr-code from here?

@yogo1212
Copy link

yogo1212 commented Nov 28, 2024

the exact same command (taken from my shell history) produces different outcomes.
images that haven't been working before start working and the other way around.

this is the kind of thing that drives you crazy. at least there's my colleague who confirmed the qr code isn't working for him as well (he was the one who triggered the investigation because he tested a totp API and originally thought that there was something with the API checking the generated totp code).

the qr code that prompted me to join this discussion was generated by https://github.com/piglig/go-qr and shown on a web frontend (no qrencode). other apps work. this is 100% a "google authenticator" issue.

since that is not open source, i'm choosing not to work on this anymore (probing a black box is no fun). this is a "tell your customers that google authenticator doesn't work reliably" situation.

@KEINOS
Copy link

KEINOS commented Dec 2, 2024

@yogo1212

to me, it was unclear whether we were talking about the same thing,
**snip**

Glad that now we are on the same page! And I did reproduce your error for the first time! 🎉

@KEINOS does your google authenticator eat that qr-code from #94 (comment)?

Indeed, I fed to my GoogleAuthenticator and got food poisoning as expected! 😄

Reading the QR code image that you provided;
On iPhone, it throws an error thats telling to use the iPhone or iPad's camera.
On Android, it does read the QR code. But does NOT generate the passcode. It keeps showing "_ _ _ _ _ _" (6 digit).

I also analyzed the QR code image from the DENSO Wave's QR reader and it returned the below URI, which is the exact same URI.

otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X`

Even though more digging is needed, I have a feeling that "error correction" of the QR code is causing the unstable-ness.

What version of qrencode are you using? > @yogo1212

@yogo1212
Copy link

yogo1212 commented Dec 4, 2024

@KEINOS i was about to test whether the order of the parameters still held as reason for the failure to parse the qr code.
it appears you had the same hunch as me. with your confirmation, i'm closing my pr to change the order of the parameters.

depending on the rest of the qr code's content (issuer etc.), the secret parameter can influence the 'version' of the qr code when using qrencode.
for my test string, it was version 7 with the code at the back and version 10 with the code in front - iirc.

my patience was exhausted when the very same qr code image (png) which was not working earlier started working out of the blue and then, later, stopped working again. i don't want to spend days scanning a series of qr codes with the google authenticator before i know whether a qr code is good or not.

one successful scan is not enough to determine whether a qr code works or not

i don't know how much time your willing to invest in this. we don't know what is causing the error message. we don't even know whether the image parser or the uri parser is causing it. depending on which one it is, further testing looks very different. someone with source access can debug this issue efficiently. we can not (assuming you don't have access to the source code).

@KEINOS
Copy link

KEINOS commented Dec 7, 2024

I did some digging. And yes, GoogleAuthenticatior su*ks.

As a conclusion, THERE IS A PARSING BUG in GoogleAuthenticator's QR Code image decoder. And we can avoid that bug by appending a parameter "&gauthsucks" in the URI.

Yea, really. In bash, try:

uriTailSec="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"

qrencode -t PNG -o ./qr_tailing_sec_A.png -l Q <<< "${uriTailSec}" # errors
qrencode -t PNG -o ./qr_tailing_sec_B.png -l Q <<< "${uriTailSec}&gauthsucks" # succeeds

tl;dr

I found that it was the other way around. It is not that the "secret" parameter should be placed at the beginning of the URI, but rather that it should not be placed at the end in some occasions.

With GoogleAuthenticatior;

  1. The order of the parameter in the URI itself doesn't matter.

  2. But depending on the combination of the below settings of QR code, GoogleAuthenticatior fails to parse if the "secret" parameter comes at the end of the URI. When reading the value of the "secret" key, it no longer seem to know when to stop reading. Thus, errors.

    • Standard using to draw the QR code (ISS or JEIDA or ISO/IEC1800 or JIS)
    • Number of modules in the QR code
    • QR Code symbol version (1-40)
    • Encoding mode (it should be Binary (8 bit or byte))
    • ECC level
    • Mask level
    • Note: The above settings vary, in particular the number of modules, depending on the length of the data to be encoded. That is why this problem is so complex.
  3. To avoid this, simply append an extra parameter in the URI. Such as "&foo" or "&yourappname" or "&app=v2" or "&gauth=sucks" or whatever.

    otpauth://totp/example.com:[email protected]?\
      algorithm=SHA256&\
      digits=8&\
      issuer=example.com&\
      period=45&\
    - secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X
    + secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&foo

The solution is simple, but thanks to @yogo1212 and @Cprime50 for issueing out the problem. Because of this issue, I was able to fix the bug before any potential issues arose. Thank you very much.

My customers trust Google more than I do. So using GoogleAuthenticator is a must. They don't listen to me because they think such a bug can't possibly exist in an app of a security-conscious company.

My Opinion to fix this issue

Force append "&google" (or something) in internal.EncodeQuery(). Since this function is for GoogleAuthenticator compatibility in the first place. What do you think? > @pquerna

// EncodeQuery is a copy-paste of url.Values.Encode, except it uses %20 instead
// of + to encode spaces. This is necessary to correctly render spaces in some
// authenticator apps, like Google Authenticator.
func EncodeQuery(v url.Values) string {
	if v == nil {
		return ""
	}
	var buf strings.Builder
	keys := make([]string, 0, len(v))
	for k := range v {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		vs := v[k]
		keyEscaped := url.PathEscape(k) // changed from url.QueryEscape
		for _, v := range vs {
			if buf.Len() > 0 {
				buf.WriteByte('&')
			}
			buf.WriteString(keyEscaped)
			buf.WriteByte('=')
			buf.WriteString(url.PathEscape(v)) // changed from url.QueryEscape
		}
	}
+	// Append extra parameter for GoogleAuthenticator parsing bug
+	// (Fix #94)
+	buf.WriteString("&google")

	return buf.String()
}

ts;dr

Parameter Order

First of all, we have to make sure if the order of "secret" parameter in the URI matters to read on GoogleAuthenticator.

The answer was NO.

To prove that, the below three images can be read with GoogleAuthenticator, and all account generates the same passcode as the other SHA256 supported TOTP apps does. (Note the parameter values and the order in the URIs as well.)

Heading secret
qr_go_heading_sec
otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45
Middle secret
qr_go_middle_sec
otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45
Tailing secret
qr_go_tailing_sec
otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X

If GoogleAuthenticator restricts the order of the "secret" parameter, the last two QR codes should error, but they didn't.

In other words, the order of the parameters in the URI does not matter as long as the QR code can be read correctly. Also at this point, we can confirm that GoogleAuthenticator does support SHA-256 as well.

Go code to reproduce the above image

Note that we are not using our pquerna/otp, but the boombuler/barcode package on which it depends.

package main

import (
	"image"
	"image/png"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	const (
		// URI which begins with a secret parameter
		uriHeadSec = "otpauth://totp/domain.com:[email protected]?" +
			"secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45"
		// URI which a secret parameter is in the middle
		uriMiddleSec = "otpauth://totp/domain.com:[email protected]?" +
			"algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45"
		// URI which ends with a secret parameter
		uriTailSec = "otpauth://totp/domain.com:[email protected]?" +
			"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"
		imgSize = 195 // 195x195 pixels
	)

	// Generate QR code images
	const (
		qr_level = qr.Q       // Error correction level (choices: L, M, Q, H)
		qr_mode  = qr.Unicode // Not only UPPER case
	)

	imgHeadSec, err := customImage(uriHeadSec, qr_level, qr_mode, imgSize, imgSize)
	panicOnError(err)

	imgMiddleSec, err := customImage(uriMiddleSec, qr_level, qr_mode, imgSize, imgSize)
	panicOnError(err)

	imgTailSec, err := customImage(uriTailSec, qr_level, qr_mode, imgSize, imgSize)
	panicOnError(err)

	// Save the QR code to a file
	panicOnError(saveImg(imgHeadSec, "qr_go_heading_sec.png"))
	panicOnError(saveImg(imgMiddleSec, "qr_go_middle_sec.png"))
	panicOnError(saveImg(imgTailSec, "qr_go_tailing_sec.png"))
}

// Note "mode"(encoding mode) are: qr.Auto, qr.AlphaNumeric, qr.Numeric, qr.Unicode
// But `qr.AlphaNumeric` only encodes:
//
//	UPPERCASE letters, numbers and `[Space], $, %, *, +, -, ., /, :`.
//
// Therefore, we must use `qr.Auto` or `qr.Unicode` instead of `qr.AlphaNumeric` to
// encode as bytes (as is).
//
// See: https://en.wikipedia.org/wiki/QR_code#Information_capacity
func customImage(content string, level qr.ErrorCorrectionLevel, mode qr.Encoding, width int, height int) (image.Image, error) {
	b, err := qr.Encode(content, level, mode)
	if err != nil {
		return nil, err
	}

	b, err = barcode.Scale(b, width, height)
	if err != nil {
		return nil, err
	}

	return b, nil
}

func panicOnError(err error) {
	if err != nil {
		panic(err)
	}
}

func saveImg(img image.Image, filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}

	defer file.Close()

	if err := png.Encode(file, img); err != nil {
		return err
	}

	return nil
}

QR Code Generation

The next question is whether there are any problems with the creation of QR codes. This is also the reason why the topic is so complex.

The QR codes shown previously ware generated in Go with the package boombuler/barcode and works well.

However, if a QR code with the same URI is generated with the qrencode command, GoogleAuthenticator starts to fail reading.

The first two (heading and middle secret) succeeds reading but the last one.

Heading secret (w/qrencode) -> SUCCEED
qr_bash_heading_sec
otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45
Middle secret (w/qrencode) -> SUCCEED
qr_bash_middle_sec
otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45
Tailing secret (w/qrencode) -> FAILURE
qr_bash_tailing_sec
otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X

But, at this point, we know that the order of the "secret" parameters proved to be irrelevant.

This means that the problem can be narrowed down to either; a problem with the QR code generation with "qrencode" or a reading problem with GoogleAuthenticator.

So, did "qrencode" inadvertently generate the wrong QR code under niche conditions?

I'd say the answer as NO.

Because, as scanning the failing QR code (w/tailing secret param) with the below apps, all apps read the QR code fine.

  • The official DENSO Wave's QR reader app from arara Inc.
    • It displayed the same URI as the original
  • Apple Password app via iPhone camera
    • Once adding a dummy user name and password to a account, it generates the right passcode
  • qrscan command
    • Giving the generated QR code image, it returns the same URI as the original

This means that the QR code generated via qrencode CAN be read and decoded (not corrupted).

[!NOTE]
Above all, however, the library "libqrencode", on which "qrencode" is based, is widely used and well tested.

The only concern is that the library has not been updated for more than four years and may not meet the current ISO/IEC 18004:2024 standards of the QR code. But this is not a critical point for backward compatibility per se.

Bash script to reproduce the above image
#!/bin/bash

set -e

type qrencode || { echo "qrencode not found"; exit 1; }

qrencode --version

#rm -f ./qr_*.png

# URI which begins with a secret parameter
uriHeadSec="otpauth://totp/domain.com:[email protected]?secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45"
# URI which a secret parameter is in the middle
uriMiddleSec="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45"
# URI which ends with a secret parameter
uriTailSec="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"

qrencode -t PNG -o ./qr_bash_heading_sec.png -l Q <<< "$uriHeadSec"
qrencode -t PNG -o ./qr_bash_middle_sec.png -l Q <<< "$uriMiddleSec"
qrencode -t PNG -o ./qr_bash_tailing_sec.png -l Q <<< "$uriTailSec"

Error Correction

So far, the combination of URI with tailing "secret" parameter and qrencode causes the problem.

I then assumed that there might be a problem with error correction when scanning with GoogleAuthenticator.

I created QR codes with tailing secret URIs with error correction levels L, M, Q and H. But none of them could be read by GoogleAuthenticator.

This proves that scanning error is not causing the problem rather than parsing the image.

Script to reproduce
#!/bin/bash

set -e

uriTailSec="otpauth://totp/domain.com:[email protected]?algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X"

qrencode -t PNG -o ./qr_tailing_sec_L.png -l L <<< "$uriTailSec"
qrencode -t PNG -o ./qr_tailing_sec_M.png -l M <<< "$uriTailSec"
qrencode -t PNG -o ./qr_tailing_sec_Q.png -l Q <<< "$uriTailSec"
qrencode -t PNG -o ./qr_tailing_sec_H.png -l H <<< "$uriTailSec"

qrencode -t PNG -o ./qr_tailing_sec_L_2.png -l L <<< "${uriTailSec}&gauthsucks"
qrencode -t PNG -o ./qr_tailing_sec_M_2.png -l M <<< "${uriTailSec}&gauthsucks"
qrencode -t PNG -o ./qr_tailing_sec_Q_2.png -l Q <<< "${uriTailSec}&gauthsucks"
qrencode -t PNG -o ./qr_tailing_sec_H_2.png -l H <<< "${uriTailSec}&gauthsucks"

@KEINOS
Copy link

KEINOS commented Dec 8, 2024

Simply adding an extra "&" delimiter at the end fixes the problem as well.
Since number of characters embedded in the QR code matters, the shorter the better.

	// Append extra parameter for GoogleAuthenticator parsing bug
	// (Fix #94)
-	buf.WriteString("&google")
+	buf.WriteString("&")

@yogo1212
Copy link

yogo1212 commented Dec 9, 2024

@KEINOS wow!
the & looks like a really good idea 👍
it changed my qr code with 'production' values into something that works as well.

so far, at least. i'll be checking again tomorrow because i'm not approaching this problem without a sheet of aluminium foil on my noggin.

@KEINOS
Copy link

KEINOS commented Dec 10, 2024

I’m glad the simple “&” managed to break our QR code (Google Authenticator) curse ! 🎉

Let’s keep the aluminium foil close, though—one can never be too careful when it comes to production mysteries.

Looking forward to hearing tomorrow’s results. Thanks for giving it a shot!

@yogo1212
Copy link

still works.
apple keychain is also happy with the & at the end

@KEINOS
Copy link

KEINOS commented Dec 15, 2024

Here's my two cents for those who want the "GoogleAuthenticator-safe URI" for an external application to generate QR code images.

// Fix94GAuthBug appends an extra parameter delimiter "&" to the URI if necessary.
//
// This function provides a workaround for a known bug in Google Authenticator.
// (See issue #94: https://github.com/pquerna/otp/issues/94#issuecomment-2524954588 )
//
// The bug affects QR codes generated with certain combinations of standards
// and parameters, causing them to fail when scanned by Google Authenticator.
// Use this function if you need to generate a QR code using an external tool
// (outside this library) and require the URI to be compatible with Google Authenticator.
// It adjusts the URI to ensure proper functionality.
func Fix94GAuthBug(uri string) string {
	chunks := strings.Split(uri, "&")

	if !strings.HasPrefix(chunks[len(chunks)-1], "secret=") {
		return uri // last param is not a secret
	}

	return uri + "&" // avoid GoogleAuthenticator bug
}
Tests
func TestFix94GAuthBug(t *testing.T) {
	for index, test := range []struct {
		title     string
		uriInput  string
		uriExpect string
	}{
		{
			title: "heading secret",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&algorithm=SHA256&digits=8&issuer=domain.com&period=45",
		},
		{
			title: "middle secret",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&issuer=domain.com&period=45",
		},
		{
			title: "tailing secret (require '&' to be added)",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&",
		},
		{
			title: "tailing secret (fixed w/& only)",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&",
		},
		{
			title: "tailing secret (fixed w/single param)",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&foo",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&foo",
		},
		{
			title: "tailing secret (fixed w/key-value param)",
			uriInput: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&foo=bar",
			uriExpect: "otpauth://totp/domain.com:[email protected]?" +
				"algorithm=SHA256&digits=8&issuer=domain.com&period=45&secret=DEOXGYTNWD3D6J3RNBEGCI2R45X3XO3X&foo=bar",
		},
	} {
		expect := test.uriExpect
		actual := Fix94GAuthBug(test.uriInput) // Apply bug fix

		if expect != actual {
			t.Errorf("test case #%d: %s", index+1, test.title)
		}
	}
}

By the way, @Cprime50, have you solved your problem? Still not working?

@yogo1212
Copy link

@KEINOS would you mind turning that into a PR?

KEINOS added a commit to KEINOS/Fork_otp that referenced this issue Dec 25, 2024
KEINOS added a commit to KEINOS/Fork_otp that referenced this issue Dec 25, 2024
@KEINOS KEINOS linked a pull request Dec 25, 2024 that will close this issue
@KEINOS
Copy link

KEINOS commented Dec 26, 2024

@yogo1212 (CC: @Cprime50 , @pquerna )

Happy holidays!

I created a PR #99 to close this issue.

Since the bug itself does not rely on our package, I have not changed the function internal.EncodeQuery().
I only added a function to adjust the URI to avoid GAuth's bug, so that the user can decide. And added the how-to in the readme.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants