TOTP: Secret / Arduino HEX Array / Base32

Hello,

I am trying to integrate Lucadentella's TOTP-Arduino library

The usage seems to be straight forward. You have to have the current UNIX Epoch time stamp and the “Secret” of the key you want to generate, in form of a char array. This, put together should generate the 6 digit number code as known from Google Authenticator or MS Authenticator Apps.

I finished my implementation and added a very easy secret test:
“0123” -> {0x30, 0x31, 0x32, 0x33}
I set up the Authenticator app, I synced the time on my target hardware and… perfect. I get the same codes as the Google Authenticator App shows. Cool.

Then I startet to experiment and use a real secret that is used for a google account login.
I won’t post it here but I just can say that it is much longer, 32 digits long, and contains numbers and lower case letters coded as ASCII characters. I tried to use it exactly the same way as before by putting them to a char array. The result is that I get different numbers than on my Google Authenticator App. Bad.
Then I found out that my TOTP secret code for my BitMEX account is completely different. It contains numbers and UPPER case letters and is 16 digits long in total. Testing this secret code also has not resulted in the generation of the correct OTPs.

So now I am confused.
1.) It seems that secret codes can have diffent codings and lengths. Additionally I have noticed on OTP Tool for Arduino TOTP Library that only secrets with a length of 20 characters max are accepted. So is there a limitation and why is my google code longer? Do I have to convert it first?
2.) All the 2FA apps are accepting these different secrets flawlessly. Is it that these apps detect the secret format and convert them before usage? Or maybe I am wrong these different secret formats are just format prefers by the issuers all should work fine the same way?
3.) What’s about Base32? I thought maybe the secrets have to be decoded with Base32 before I can use them directly as an char array? Had no luck.
4.) Are there any library limitations or something else to keep in mind? Maybe the library can just handle secrets with a special length?
5.) Maybe there is a bug in the library? Anyone knows?

Hope someone can help.
All the best!

https://datatracker.ietf.org/doc/html/rfc6238t

Look like the 'secret' come in three styles:
SHA1 (20 bytes)
SHA256 (32 bytes)
SHA512 (64 bytes)

I just found out that the "0123" code was not used by directly in the app.
It seems that it was encoded over Base32
from "0123"
to "GAYTEMY="

When you put "GAYTEMY=" into an Authenticator App it will show the same OTPs as the "0123" configured Arduino.

So maybe I have to convert my existing Secrets the other way arround and DECODE them regards Base32. The result is (maybe) the correct value for the char array?!

Have to understand Base32 first and then test it out.

Sry, can't open your link but currently I am guessing that it may has something to do with Base32 encoding/decoding.

Hmmm....

The Website

Can't decode the original secrets:

"Error: Decoded data is not valid UTF-8. Maybe try base32.decode.asBytes()? Partial data after reading 3 bytes: > <-"

and

"Error: Invalid base32 characters"

I figured it out.

Here is what I have learned:

The secret itself is also called "hmacKey" and has to be configured while initializing the TOTP module.
It must have to have a binary byte array style and the data is completely unformatted. One could discuss if it is byte or character, unsigned or signed, however for me this is clearly a unsigned byte array data since the values can be from 0-255 and have no useful ASCII representation.

In the internet, QR codes are commonly used to configure a secret. This QR code contains a “pseudo URL” where just one parameter is the secret itself. Since an URL can’t hold all 0-255 character/byte values, the values have to be encoded with Base32. The encoded output should technically contain characters 0-7, A-Z (upper case) and ‘=’ only. Seems that the internet does not take it so serious with this and in reality Base32 encoded secrets are also served with a-z (lower case) and spaces ‘ ‘ but this is clearly not part of the legal Base32 character base (I guess).
This is why we have many different formats for encoded secrets.

So, what to do, when you have a encoded secret and you want to implement a HW TOTP with Arduino?
1.) Remove all spaces from the encoded secret
2.) Convert all a-z characters to upper case (A-Z)
3.) DECODE the secret regarding Base32. Be aware of that the result is NOT an ASCII string. It is binary data that is not viewable in a normal text editor. I recommend using an unsigned byte array. Typically, it is 10 or 20 bytes long.
4.) Initialize the TOTP Arduino module with the decoded secret array.
5.) Sync up the time…
6.) Generate the 6-digit number codes.

It seems that the commonly used TOTPs are all working with SHA1. At least for me, every secret I have tested seems to work.
Hope this helps someone.

Hi, I'm new to totp and i'm making a project with totp/google authenticator but the generated totp code doesn't match with the one in google authenticator, could anyone tell what i'm doing wrong? (I used '0123' as secret key as example)
This is my code:

#include <Keypad.h>
#include "RTClib.h"
#include <sha1.h>
#include <TOTP.h>

uint8_t hmacKey[] = {0x30, 0x31, 0x32, 0x33};
TOTP totp = TOTP(hmacKey, 4);

const byte rows = 4;
const byte cols = 3;
char keys[rows][cols] = {
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};
byte rowPins[rows] = {2, 3, 4, 5};
byte colPins[cols] = {6, 7, 8};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, rows, cols);

RTC_DS1307 rtc;
char* totpCode;
char inputCode[7];
int inputCode_idx;

void setup() {
  Serial.begin(9600);
  rtc.begin();

  if (!rtc.isrunning()) {
    Serial.println("RTC not found!");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  } else {
    Serial.println("RTC initialized and started!");
  }

  inputCode_idx = 0;

  Serial.println("Door closed");

}

void loop() {
  
  char key = keypad.getKey();


  if (key != NO_KEY) {


    if(key == '#') {
      Serial.println("# pressed, resetting the input buffer...");
      inputCode_idx = 0;      
    }
    
    else if(key == '*') {

        Serial.println("* pressed, closing the door...");
    }
    else {      
      
      // save key value in input buffer
      inputCode[inputCode_idx++] = key;
      
      // if the buffer is full, add string terminator, reset the index
      // get the actual TOTP code and compare to the buffer's content
      if(inputCode_idx == 6) {
        
        inputCode[inputCode_idx] = '\0';
        inputCode_idx = 0;
        Serial.print("New code inserted: ");
        Serial.println(inputCode);
        
        DateTime now = rtc.now();
        long GMT = now.unixtime();
        totpCode = totp.getCode(GMT);
        
        // code is ok :)
        if(strcmp(inputCode, totpCode) == 0) {   

            Serial.println("Code ok, opening the door...");

          
        // code is wrong :(  
        } else {
          Serial.print("Wrong code... the correct was: ");
          Serial.println(totpCode);         
        }
      }      
    }
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.