Skip to content

TOTP token does not work when secret is base 32 encoded #153

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

Open
StevenJL opened this issue Mar 13, 2025 · 1 comment
Open

TOTP token does not work when secret is base 32 encoded #153

StevenJL opened this issue Mar 13, 2025 · 1 comment

Comments

@StevenJL
Copy link

StevenJL commented Mar 13, 2025

When the encoding is done with base32, the TOTP token fails (incorrectly)

Reproduction Steps

import speakeasy from 'speakeasy'
import qrcode from 'qrcode'

var secret = speakeasy.generateSecret();
const otpAuthUrl = speakeasy.otpauthURL({
  secret: secret.base_32,
  label: 'teemo@yordel.org',
  issuer: 'Yordel Inc',
  algorithm: 'sha1',
  digits: 6,
});
const qrCodeImage = await qrcode.toDataURL(otpAuthUrl);

// Now scan the qrCodeImage to Google Authenticator and get the OTP and assign to `otp`

var otp = '123123' // get it from Google Authenticator

const otpIsValid = speakeasy.totp.verify({
  secret: secret.base_32,
  encoding: 'base32',
  algorithm: 'sha1',
  token: otp,
  window: 1,
});

console.log(otpIsValid)
// returns false

Now, contrast this the ascii encoding, which does work

import speakeasy from 'speakeasy'
import qrcode from 'qrcode'

var secret = speakeasy.generateSecret();
const otpAuthUrl = speakeasy.otpauthURL({
  secret: secret.ascii,
  label: 'teemo@yordel.org',
  issuer: 'Yordel Inc',
  algorithm: 'sha1',
  digits: 6,
});
const qrCodeImage = await qrcode.toDataURL(otpAuthUrl);

// Now scan the qrCodeImage to Google Authenticator and get the OTP and assign to `otp`

var otp = '123123' // get it from Google Authenticator

const otpIsValid = speakeasy.totp.verify({
  secret: secret.ascii,
  encoding: 'ascii',
  algorithm: 'sha1',
  token: otp,
  window: 1,
});

console.log(otpIsValid)
// returns true
@StevenJL
Copy link
Author

Work-Around

I found a way to still use speakeasy for my TFA feature using base32 encoding (due to legacy reasons).

Instead of relying on speakeasy to generate the secret and otpAuthUrl, you generate these using a combo of crypto, base32 and url.

import crypto from 'crypto'
import base32 from 'hi-base32'
import url from 'url'

const generateBase32Secret = () => {
  var randomBytes = crypto.randomBytes(20)
  const base32String = base32.encode(randomBytes);
  return base32String
}

const base32Secret = generateBase32Secret()

const generateOtpAuthUrl = (base32Secret, label, issuer, digits) => {
  const query = {
    secret: base32Secret,
    issuer: issuer,
    digits: digits,
  }
  return url.format({
    protocol: 'otpauth',
    slashes: true,
    hostname: 'totp',
    pathname: label,
    query: query
  })
}

const otpAuthUrl = generateOtpAuthUrl(base32Secret, 'teemo@yordel.org', 'TopLane', 6)
const qrCode = await qrcode.toDataURL(otpAuthUrl);

// Now scanning the qrCode and adding this new entry in an authenticator app, you can use speakeasy for verifying it.


var otp = '466719' // get this from your authenticator app

const otpIsValid = speakeasy.totp.verify({
  secret: base32Secret
  encoding: 'base32',
  token: otp,
  window: 1,
});

console.log(otpIsValid)
// true

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

No branches or pull requests

1 participant