generateSecret produces base32 secrets with invalid length
Description
generateSecret() occasionally produces base32 secrets whose length is not a multiple of 8, which causes totp.verify() and totp.verifyDelta() to silently return false/undefined even when the token is correct.
Steps to Reproduce
const speakeasy = require("speakeasy");
for (let i = 0; i < 10; i++) {
const secret = speakeasy.generateSecret({ otpauth_url: true });
console.log(secret.base32.length, secret.base32.length % 8 === 0 ? "OK" : "INVALID");
}
Example Output
52 INVALID
55 INVALID
56 OK
52 INVALID
48 OK
55 INVALID
Expected Behavior
secret.base32 should always have a length that is a multiple of 8, as required by the base32 spec.
Actual Behavior
secret.base32 length varies between runs and is frequently not a multiple of 8. When an invalid secret is stored and later passed to totp.verify(), it always returns false regardless of the token being correct.
Workaround
Pad the secret before verifying:
function padBase32(secret) {
const pad = secret.length % 8;
return pad ? secret + "=".repeat(8 - pad) : secret;
}
speakeasy.totp.verify({
secret: padBase32(key.secret),
encoding: "base32",
token: code,
});
Environment
- speakeasy: 2.0.0
- Node.js: v24.13.0
generateSecretproduces base32 secrets with invalid lengthDescription
generateSecret()occasionally produces base32 secrets whose length is not a multiple of 8, which causestotp.verify()andtotp.verifyDelta()to silently returnfalse/undefinedeven when the token is correct.Steps to Reproduce
Example Output
Expected Behavior
secret.base32should always have a length that is a multiple of 8, as required by the base32 spec.Actual Behavior
secret.base32length varies between runs and is frequently not a multiple of 8. When an invalid secret is stored and later passed tototp.verify(), it always returnsfalseregardless of the token being correct.Workaround
Pad the secret before verifying:
Environment