Google Authenticator code does not match server generated code

2024/9/23 8:02:05

Background


I'm currently working on a two-factor authentication system where user are able to authenticate using their smartphone. Before the user can make use of their device they need to verify it first. For this they need to scan a QR code I give them and enter the code that subsequently shows.

Problem


The scanning of the QR code works fine, and it gets read correctly by the Google Authenticator app. However, the generated codes don't match with the ones I'm generating on the server.

What have I tried


I have tried a couple of things in the hope of finding my problem.

  1. I have tried directly inserting both a default secret: 'thiswasmysecretkeyused' and a base64.b32encode() encoded version of the secret: 'ORUGS43XMFZW26LTMVRXEZLUNNSXS5LTMVSA====' in the Google Authenticator app, but these both generated codes different from the server.

  2. I read that the trailing ==== from the key may cause it to not work, so I tried adding one without those as well. Still no good results (they generate the same codes)

  3. I have tried using a different algorithm for generating TOTP codes, for in the unlikely event that the algorithm I'm using (django-otp) is incorrect. The different algorithm I used was taken from this answer. Both algorithms generated the same codes when using the same key.

  4. I checked what the time on my system was. I saw that the operating system was showing 15:03 just like my smartphone was. After dumping the time in python with both time.time() and datetime.datetime.now() I saw the returned time was one hour behind the operating system time; showing 14:03. I tried adding 3600 seconds in to the timestamp used for code generation, but to no avail.

  5. I have tried several other things, but can't quite recall what they all were.

  6. I've looked up the code that accepts keys in Google Authenticator and have verified that it is expecting a base32 string. So my encoding of the key is correct, as far as I'm aware. From the code (EnterKeyActivity.java, line 78):

    Verify that the input field contains a valid base32 string

Code


Generating the secret key;

def generate_shared_key(self):# create hash etc.return base64.b32encode(hasher.hexdigest())

Generating the QR code;

key = authenticator.generate_shared_key()
qrcode = pyqrcode.create('otpauth://totp/someurl.nl?secret=' + key)

Generating the TOTP code;

def generate_code(self, drift_steps=0, creation_interval=30, digits=6, t0=0):code = str(totp(self.generate_shared_key(), creation_interval, timestamp, digits, drift_steps))return code.zfill(digits)

If you need any more code, such as django-otp actual totp generating code, let me know.

Errors


No errors.

Hunches


My hunch is that I must be going wrong somewhere with the key generation or with passing the key to Google Authenticator. Since even manually putting the key in Google Authenticator fails to generate the correct codes. Does Google Authenticator do something more with the key once it's been saved, such as adding an user?

I also noticed in the other algorithm I used that the secret there gets decoded first with;

key = base64.b32decode(secret, True) 

Is my original key (a SHA512 hash) wrong? Should I or should I not encode it with base64.b32encode()? If I try to scan the QR code generated without encoding the hash, Google Authenticator says it does not recognize it as a (valid) key.

Answer

Alright, after digging through the code of Google Authenticator I finally found what I was doing wrong.

Key Encoding

Just so it's clear: Google Authenticator does expect a base32 encoded string as a secret. So whether you enter it manually or via a QR code, you have to make sure your secret is a base32 encoded string when you give it to Google Authenticator.

From EnterKeyActivity:

/** Verify that the input field contains a valid base32 string,* and meets minimum key requirements.*/
private boolean validateKeyAndUpdateStatus(boolean submitting) {//...
}

Storing

Google Authenticator is storing the key you give it in the database as is. So this means it stores the base32 string of your secret directly in the database.

From EnterKeyActivity:

private String getEnteredKey() {String enteredKey = mKeyEntryField.getText().toString();return enteredKey.replace('1', 'I').replace('0', 'O');
}protected void onRightButtonPressed() {//...if (validateKeyAndUpdateStatus(true)) {AuthenticatorActivity.saveSecret(this, mAccountName.getText().toString(), getEnteredKey(), null, mode, AccountDb.DEFAULT_HOTP_COUNTER);exitWizard();}//...
}

From AuthenticatorActivity:

static boolean saveSecret(Context context, String user, String secret, String originalUser, OtpType type, Integer counter) {//...if (secret != null) {AccountDb accountDb = DependencyInjector.getAccountDb();accountDb.update(user, secret, originalUser, type, counter);//...}
}

Retrieval

When Google Authenticator retrieves the secret from the database, it decodes the base32 string so it can use the genuine secret.

From OtpProvider:

private String computePin(String secret, long otp_state, byte[] challenge) throws OtpSourceException {//...try {Signer signer = AccountDb.getSigningOracle(secret);//...}
}

From AccountDb:

static Signer getSigningOracle(String secret) {try {byte[] keyBytes = decodeKey(secret);//...}
}private static byte[] decodeKey(String secret) throws DecodingException {return Base32String.decode(secret);
}

Mistake

My mistake was that, on the server-side, I was using the base32 encoded key for generating TOTP codes, since I thought Google Authenticator used that as well. In hindsight it's of course very logical, but I couldn't find too much info about this. Hopefully this will help out some more people in the future.

TL;DR

Make sure the secret/key you pass to Google Authenticator is a base32 encoded string. Make sure that on the server side you're not using the base32 encoded string, but the decoded string. In Python you can encode and decode you secret/key as follows:

import base64base64.b32encode(self.key)
base64.b32decode(self.key)
https://en.xdnf.cn/q/71822.html

Related Q&A

Gekko Non-Linear optimization, object type error in constraint function evaluating if statement

Im trying to solve a non-linear optimization problem. Ive duplicated my issue by creating the code below. Python returns TypeError: object of type int has no len(). How can I include an IF statement in…

Store large dictionary to file in Python

I have a dictionary with many entries and a huge vector as values. These vectors can be 60.000 dimensions large and I have about 60.000 entries in the dictionary. To save time, I want to store this aft…

Python: override __str__ in an exception instance

Im trying to override the printed output from an Exception subclass in Python after the exception has been raised and Im having no luck getting my override to actually be called.def str_override(self):…

How hide/show a field upon selection of a radio button in django admin?

models.pyfrom django.db import models from django.contrib.auth.models import UserSTATUS_CHOICES = ((1, Accepted),(0, Rejected),) class Leave(models.Model):----------------status = models.IntegerField(c…

format/round numerical legend label in GeoPandas

Im looking for a way to format/round the numerical legend labels in those maps produced by .plot() function in GeoPandas. For example:gdf.plot(column=pop2010, scheme=QUANTILES, k=4)This gives me a lege…

Python pickle crash when trying to return default value in __getattr__

I have a dictionary like class that I use to store some values as attributes. I recently added some logic(__getattr__) to return None if an attribute doesnt exist. As soon as I did this pickle crashe…

How to download google source code for android

As you know, there is a list of several hundred projects in https://android.googlesource.com/. Id like to download them all in windows machine. According to Googles document,To install, initialize, and…

Compute on pandas dataframe concurrently

Is it feasible to do multiple group-wise calculation in dataframe in pandas concurrently and get those results back? So, Id like to compute the following sets of dataframe and get those results one-by…

How do I go about writing a program to send and receive sms using python?

I have looked all over the net for a good library to use in sending and receiving smss using python but all in vain!Are there GSM libraries for python out there?

Persist Completed Pipeline in Luigi Visualiser

Im starting to port a nightly data pipeline from a visual ETL tool to Luigi, and I really enjoy that there is a visualiser to see the status of jobs. However, Ive noticed that a few minutes after the l…