diff --git a/index.js b/index.js index f139b4a..f151fe2 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,8 @@ */ import * as crypto from 'node:crypto' -/** - * @type {{ encode: (data: string | import('buffer').Buffer) => string, decode: (data: string) => import('buffer').Buffer }} - */ -import * as base32 from 'thirty-two' +import base32Encode from 'base32-encode' +import base32Decode from 'base32-decode' // SHA1 is not secure, but in the context of TOTPs, it's unrealistic to expect // security issues. Also, it's the default for compatibility with OTP apps. @@ -118,7 +116,7 @@ function verifyHOTP( * @param {string} [options.charSet='0123456789'] - The character set to use, defaults to the numbers 0-9. * @param {string} [options.secret] The secret to use for the TOTP. It should be * base32 encoded (you can use https://npm.im/thirty-two). Defaults to a random - * secret: base32.encode(crypto.randomBytes(10)).toString(). + * secret: base32Encode(crypto.randomBytes(10), 'RFC4648'). * @returns {{otp: string, secret: string, period: number, digits: number, algorithm: string, charSet: string}} * The OTP, secret, and config options used to generate the OTP. */ @@ -126,10 +124,10 @@ export function generateTOTP({ period = DEFAULT_PERIOD, digits = DEFAULT_DIGITS, algorithm = DEFAULT_ALGORITHM, - secret = base32.encode(crypto.randomBytes(10)).toString(), + secret = base32Encode(crypto.randomBytes(10), 'RFC4648'), charSet = DEFAULT_CHAR_SET, } = {}) { - const otp = generateHOTP(base32.decode(secret), { + const otp = generateHOTP(base32Decode(secret, 'RFC4648'), { counter: getCounter(period), digits, algorithm, @@ -205,7 +203,15 @@ export function verifyTOTP({ charSet, window = DEFAULT_WINDOW, }) { - return verifyHOTP(otp, base32.decode(secret), { + let decodedSecret + try { + decodedSecret = base32Decode(secret, 'RFC4648') + } catch (error) { + // If the secret is invalid, return null + return null + } + + return verifyHOTP(otp, Buffer.from(decodedSecret), { counter: getCounter(period), digits, window, diff --git a/index.test.js b/index.test.js index 61adb9a..5c4c086 100644 --- a/index.test.js +++ b/index.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert' import { test } from 'node:test' -import base32 from 'thirty-two' +import base32Encode from 'base32-encode' import { generateTOTP, getTOTPAuthUri, verifyTOTP } from './index.js' test('OTP can be generated and verified', () => { @@ -17,7 +17,10 @@ test('options can be customized', () => { algorithm: 'SHA256', period: 60, digits: 8, - secret: base32.encode(Math.random().toString(16).slice(2)).toString(), + secret: base32Encode( + new TextEncoder().encode(Math.random().toString(16).slice(2)), + 'RFC4648' + ).toString(), charSet: 'abcdef', } const { otp, ...config } = generateTOTP(options) diff --git a/package-lock.json b/package-lock.json index abffdb5..9c19c15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0-semantically-released", "license": "MIT", "dependencies": { - "thirty-two": "^1.0.2" + "base32-decode": "^1.0.0", + "base32-encode": "^2.0.0" }, "devDependencies": { "@types/node": "^20.4.5" @@ -24,12 +25,31 @@ "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==", "dev": true }, - "node_modules/thirty-two": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", - "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "node_modules/base32-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz", + "integrity": "sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g==", + "license": "MIT" + }, + "node_modules/base32-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz", + "integrity": "sha512-mlmkfc2WqdDtMl/id4qm3A7RjW6jxcbAoMjdRmsPiwQP0ufD4oXItYMnPgVHe80lnAIy+1xwzhHE1s4FoIceSw==", + "license": "MIT", + "dependencies": { + "to-data-view": "^2.0.0" + }, "engines": { - "node": ">=0.2.6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/to-data-view": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz", + "integrity": "sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } } }, @@ -40,10 +60,23 @@ "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==", "dev": true }, - "thirty-two": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", - "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==" + "base32-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz", + "integrity": "sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g==" + }, + "base32-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz", + "integrity": "sha512-mlmkfc2WqdDtMl/id4qm3A7RjW6jxcbAoMjdRmsPiwQP0ufD4oXItYMnPgVHe80lnAIy+1xwzhHE1s4FoIceSw==", + "requires": { + "to-data-view": "^2.0.0" + } + }, + "to-data-view": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz", + "integrity": "sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==" } } } diff --git a/package.json b/package.json index b669dbe..6234046 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "author": "Kent C. Dodds (https://kentcdodds.com/)", "license": "MIT", "dependencies": { - "thirty-two": "^1.0.2" + "base32-decode": "^1.0.0", + "base32-encode": "^2.0.0" }, "engines": { "node": ">=18"