feat: add mfa code completion from TOTP key

This commit is contained in:
TheDevYellowy
2025-01-20 03:48:11 -06:00
parent c1f6f5e78c
commit 5fbca25c45
4 changed files with 224 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ const Intents = require('../util/Intents');
const Permissions = require('../util/Permissions');
const DiscordAuthWebsocket = require('../util/RemoteAuth');
const Sweepers = require('../util/Sweepers');
const TOTP = require('../util/Totp');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
@@ -282,14 +283,13 @@ class Client extends BaseClient {
* Logs the client in, establishing a WebSocket connection to Discord.
* @param {string} email The email associated with the account
* @param {string} password The password assicated with the account
* @param {string|number} [mfaCode = null] The mfa code if you have it enabled
* @returns {string | null} Token of the account used
*
* @example
* client.passLogin("test@gmail.com", "SuperSecretPa$$word", 1234)
* @deprecated This method will not be updated until I find the most convenient way to implement MFA.
*/
async passLogin(email, password, mfaCode = null) {
async passLogin(email, password) {
const initial = await this.api.auth.login.post({
auth: false,
versioned: true,
@@ -299,10 +299,12 @@ class Client extends BaseClient {
if ('token' in initial) {
return this.login(initial.token);
} else if ('ticket' in initial) {
if (!this.options.TOTPKey) throw new Error('TOTPKEY_MISSING');
const { otp } = await TOTP.generate(this.options.TOTPKey);
const totp = await this.api.auth.mfa.totp.post({
auth: false,
versioned: true,
data: { gift_code_sku_id: null, login_source: null, code: mfaCode, ticket: initial.ticket },
data: { gift_code_sku_id: null, login_source: null, code: otp, ticket: initial.ticket },
});
if ('token' in totp) {
return this.login(totp.token);

View File

@@ -10,6 +10,7 @@ const Messages = {
TOKEN_INVALID: 'An invalid token was provided.',
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.',
TOTPKEY_MISSING: 'Request to use mfa, but TOTPKey was not set in client options.',
WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',

194
src/util/Totp.js Normal file
View File

@@ -0,0 +1,194 @@
'use strict';
/** @typedef {"SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"} TOTPAlgorithm */
/** @typedef {"hex" | "ascii"} TOTPEncoding */
/**
* @description Options for TOTP generation
* @typedef {Object} generateOptions
* @property {number} [digits=6] - The number of digits in the OTP.
* @property {TOTPAlgorithm} [algorithm="SHA-1"] - Algorithm used for hashing.
* @property {TOTPEncoding} [encoding="hex"] - Encoding used for the OTP.
* @property {number} [period=30] - The time period for OTP validity in seconds.
* @property {number} [timestamp=Date.now()] - The current timestamp.
*/
class TOTP {
// eslint-disable-next-line valid-jsdoc
/**
* Generates a Time-based One-Time Password (TOTP)
* @async
* @param {string} key - The secret key for TOTP
* @param {generateOptions} options - Optional parameters for TOTP
* @returns {Promise<{ otp: string; expires: number }>}
*/
static async generate(key, options = {}) {
const _options = {
digits: 6,
algorithm: 'SHA-1',
encoding: 'hex',
period: 30,
timestamp: Date.now(),
...options,
};
const epochSeconds = Math.floor(_options.timestam / 1000);
const timeHex = this.dec2hex(Math.floor(epochSeconds / _options.period)).padStart(16, '0');
const keyBuffer = _options.encoding === 'hex' ? this.base32ToBuffer(key) : this.asciiToBuffer(key);
const hmacKey = await this.crypto.importKey(
'raw',
keyBuffer,
{ name: 'HMAC', hash: { name: _options.algorithm } },
false,
['sign'],
);
const signature = await this.crypto.sign('HMAC', hmacKey, this.hex2buf(timeHex));
const signatureHex = this.buf2hex(signature);
const offset = this.hex2dec(signatureHex.slice(-1)) * 2;
const masked = this.hex2dec(signatureHex.slice(offset, offset + 8)) & 0x7fffffff;
const otp = masked.toString().slice(-_options.digits);
const period = _options.period * 1000;
const expires = Math.ceil((_options.timestamp + 1) / period) * period;
return { otp, expires };
}
/**
* Converta a hexadecimal string into a decimal number
* @private
* @param {string} hex - The hexadecimal string
* @returns {number} The decimal representation
*/
static hex2dec(hex) {
return parseInt(hex, 16);
}
/**
* Converts a decimal number into a hexadeciamal string
* @private
* @param {number} dec - The decimal number
* @returns {string} The hex representation
*/
static dec2hex(dec) {
return (dec < 15.5 ? '0' : '') + Math.round(dec).toString(16);
}
/**
* Converts a base32 encoded string to an ArrayBuffer
* @private
* @param {string} str - The base32 encoded string to convert.
* @returns {ArrayBuffer} The ArrayBuffer representation of the base32 encoded string
*/
static base32ToBuffer(str) {
str = str.toUpperCase();
let length = str.length;
while (str.charCodeAt(length - 1) === 61) length--; // Remove padding
const bufferSize = (length * 5) / 8;
const buffer = new Uint8Array(bufferSize);
let value = 0,
bits = 0,
index = 0;
for (let i = 0; i < length; i++) {
const charCode = this.base32[str.charCodeAt(i)];
if (charCode === undefined) throw new Error('Invalid base32 character in key');
value = (value << 5) | charCode;
bits += 5;
if (bits >= 8) buffer[index++] = value >>> (bits -= 8);
}
return buffer.buffer;
}
/**
* Converts an ASCII string to an ArrayBuffer
* @private
* @param {string} str - The ASCII string to convert
* @returns {ArrayBuffer} The ArrayBuffer representation of the string
*/
static asciiToBuffer(str) {
const buffer = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
buffer[i] = str.charCodeAt(i);
}
return buffer.buffer;
}
/**
* Converts a hexadecimal string to an ArrayBuffer
* @private
* @param {string} hex - The hexadecimal string to convert
* @returns {ArrayBuffer} The ArrayBuffer representation of the string
*/
static hex2buf(hex) {
const buffer = new Uint8Array(hex.length / 2);
for (let i = 0, j = 0; i < hex.length; i += 2, j++) buffer[j] = this.hex2dec(hex.slice(i, i + 2));
return buffer.buffer;
}
/**
* Converts an ArrayBuffer to a hexadecimal string
* @private
* @param {ArrayBuffer} buffer - The ArrayBuffer to convert
* @returns {string} The hexadecimal string representation of the buffer
*/
static buf2hex(buffer) {
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
}
/**
* A precalculated mapping from base32 character codes to their corresponding index values for performance optimization
* This mapping is used in the base32ToBuffer method to convert base32 encoded strings to their binary representation
* @private
* @readonly
*/
static base32 = {
50: 26,
51: 27,
52: 28,
53: 29,
54: 30,
55: 31,
65: 0,
66: 1,
67: 2,
68: 3,
69: 4,
70: 5,
71: 6,
72: 7,
73: 8,
74: 9,
75: 10,
76: 11,
77: 12,
78: 13,
79: 14,
80: 15,
81: 16,
82: 17,
83: 18,
84: 19,
85: 20,
86: 21,
87: 22,
88: 23,
89: 24,
90: 25,
};
/**
* @private
* @static
* @readonly
*/
static crypto = (globalThis.crypto || require('crypto').webcrypto).subtle;
}
module.exports = TOTP;