feat: integrate otplib for TOTP authentication and remove deprecated TOTP utility
This commit is contained in:
@@ -59,6 +59,7 @@
|
|||||||
"discord-api-types": "^0.38.15",
|
"discord-api-types": "^0.38.15",
|
||||||
"fetch-cookie": "^3.1.0",
|
"fetch-cookie": "^3.1.0",
|
||||||
"find-process": "^2.0.0",
|
"find-process": "^2.0.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"prism-media": "^1.3.5",
|
"prism-media": "^1.3.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"tough-cookie": "^5.1.2",
|
"tough-cookie": "^5.1.2",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const process = require('node:process');
|
|||||||
const { setInterval } = require('node:timers');
|
const { setInterval } = require('node:timers');
|
||||||
const { setTimeout } = require('node:timers');
|
const { setTimeout } = require('node:timers');
|
||||||
const { Collection } = require('@discordjs/collection');
|
const { Collection } = require('@discordjs/collection');
|
||||||
|
const { authenticator } = require('otplib');
|
||||||
const BaseClient = require('./BaseClient');
|
const BaseClient = require('./BaseClient');
|
||||||
const ActionsManager = require('./actions/ActionsManager');
|
const ActionsManager = require('./actions/ActionsManager');
|
||||||
const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
||||||
@@ -36,7 +37,6 @@ const DataResolver = require('../util/DataResolver');
|
|||||||
const Intents = require('../util/Intents');
|
const Intents = require('../util/Intents');
|
||||||
const DiscordAuthWebsocket = require('../util/RemoteAuth');
|
const DiscordAuthWebsocket = require('../util/RemoteAuth');
|
||||||
const Sweepers = require('../util/Sweepers');
|
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.
|
* The main hub for interacting with the Discord API, and the starting point for any bot.
|
||||||
@@ -195,6 +195,18 @@ class Client extends BaseClient {
|
|||||||
*/
|
*/
|
||||||
this.readyAt = null;
|
this.readyAt = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authenticator used for TOTP
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.authenticator = authenticator;
|
||||||
|
|
||||||
|
this.authenticator.options = {
|
||||||
|
step: 30,
|
||||||
|
digits: 6,
|
||||||
|
algorithm: 'sha1',
|
||||||
|
};
|
||||||
|
|
||||||
if (this.options.messageSweepInterval > 0) {
|
if (this.options.messageSweepInterval > 0) {
|
||||||
process.emitWarning(
|
process.emitWarning(
|
||||||
'The message sweeping client options are deprecated, use the global sweepers instead.',
|
'The message sweeping client options are deprecated, use the global sweepers instead.',
|
||||||
@@ -306,7 +318,7 @@ class Client extends BaseClient {
|
|||||||
return this.login(initial.token);
|
return this.login(initial.token);
|
||||||
} else if ('ticket' in initial) {
|
} else if ('ticket' in initial) {
|
||||||
if (!this.options.TOTPKey) throw new Error('TOTPKEY_MISSING');
|
if (!this.options.TOTPKey) throw new Error('TOTPKEY_MISSING');
|
||||||
const { otp } = await TOTP.generate(this.options.TOTPKey);
|
const otp = this.authenticator.generate(this.options.TOTPKey);
|
||||||
const totp = await this.api.auth.mfa.totp.post({
|
const totp = await this.api.auth.mfa.totp.post({
|
||||||
auth: false,
|
auth: false,
|
||||||
versioned: true,
|
versioned: true,
|
||||||
|
|||||||
@@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
const Buffer = require('node:buffer').Buffer;
|
const Buffer = require('node:buffer').Buffer;
|
||||||
const { setTimeout } = require('node:timers');
|
const { setTimeout } = require('node:timers');
|
||||||
const makeFetchCookie = require('fetch-cookie');
|
const { FormData, buildConnector, Client, ProxyAgent } = require('undici');
|
||||||
const { CookieJar } = require('tough-cookie');
|
|
||||||
const { fetch: fetchOriginal, FormData, buildConnector, Client, ProxyAgent } = require('undici');
|
|
||||||
const { ciphers } = require('../util/Constants');
|
const { ciphers } = require('../util/Constants');
|
||||||
const Util = require('../util/Util');
|
const Util = require('../util/Util');
|
||||||
|
|
||||||
const cookieJar = new CookieJar();
|
|
||||||
const fetch = makeFetchCookie.default(fetchOriginal, cookieJar);
|
|
||||||
|
|
||||||
let agent = null;
|
let agent = null;
|
||||||
|
|
||||||
class APIRequest {
|
class APIRequest {
|
||||||
@@ -142,15 +137,17 @@ class APIRequest {
|
|||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref();
|
const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref();
|
||||||
return fetch(url, {
|
return this.rest
|
||||||
method: this.method.toUpperCase(), // Undici doesn't normalize "patch" into "PATCH" (which surprisingly follows the spec).
|
.fetch(url, {
|
||||||
headers,
|
method: this.method.toUpperCase(), // Undici doesn't normalize "patch" into "PATCH" (which surprisingly follows the spec).
|
||||||
body,
|
headers,
|
||||||
signal: controller.signal,
|
body,
|
||||||
redirect: 'follow',
|
signal: controller.signal,
|
||||||
dispatcher: agent,
|
redirect: 'follow',
|
||||||
credentials: 'include',
|
dispatcher: agent,
|
||||||
}).finally(() => clearTimeout(timeout));
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.finally(() => clearTimeout(timeout));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
const { setInterval } = require('node:timers');
|
const { setInterval } = require('node:timers');
|
||||||
const { Collection } = require('@discordjs/collection');
|
const { Collection } = require('@discordjs/collection');
|
||||||
|
const makeFetchCookie = require('fetch-cookie');
|
||||||
|
const { CookieJar } = require('tough-cookie');
|
||||||
|
const { fetch: fetchOriginal } = require('undici');
|
||||||
const APIRequest = require('./APIRequest');
|
const APIRequest = require('./APIRequest');
|
||||||
const routeBuilder = require('./APIRouter');
|
const routeBuilder = require('./APIRouter');
|
||||||
const RequestHandler = require('./RequestHandler');
|
const RequestHandler = require('./RequestHandler');
|
||||||
@@ -17,6 +20,8 @@ class RESTManager {
|
|||||||
this.globalRemaining = this.globalLimit;
|
this.globalRemaining = this.globalLimit;
|
||||||
this.globalReset = null;
|
this.globalReset = null;
|
||||||
this.globalDelay = null;
|
this.globalDelay = null;
|
||||||
|
this.cookieJar = new CookieJar();
|
||||||
|
this.fetch = makeFetchCookie.default(fetchOriginal, this.cookieJar);
|
||||||
if (client.options.restSweepInterval > 0) {
|
if (client.options.restSweepInterval > 0) {
|
||||||
this.sweepInterval = setInterval(() => {
|
this.sweepInterval = setInterval(() => {
|
||||||
this.handlers.sweep(handler => handler._inactive);
|
this.handlers.sweep(handler => handler._inactive);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const RateLimitError = require('./RateLimitError');
|
|||||||
const {
|
const {
|
||||||
Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
|
Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
|
||||||
} = require('../util/Constants');
|
} = require('../util/Constants');
|
||||||
const TOTP = require('../util/Totp');
|
|
||||||
|
|
||||||
const captchaMessage = [
|
const captchaMessage = [
|
||||||
'incorrect-captcha',
|
'incorrect-captcha',
|
||||||
@@ -396,7 +395,7 @@ class RequestHandler {
|
|||||||
request.retries < 1
|
request.retries < 1
|
||||||
) {
|
) {
|
||||||
// Get mfa code
|
// Get mfa code
|
||||||
const { otp } = await TOTP.generate(this.manager.client.options.TOTPKey);
|
const otp = this.manager.client.authenticator.generate(this.manager.client.options.TOTPKey);
|
||||||
this.manager.client.emit(
|
this.manager.client.emit(
|
||||||
DEBUG,
|
DEBUG,
|
||||||
`${data.message}
|
`${data.message}
|
||||||
|
|||||||
194
src/util/Totp.js
194
src/util/Totp.js
@@ -1,194 +0,0 @@
|
|||||||
'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)
|
|
||||||
* @public
|
|
||||||
* @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.timestamp / 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;
|
|
||||||
21
typings/index.d.ts
vendored
21
typings/index.d.ts
vendored
@@ -20,7 +20,6 @@ import {
|
|||||||
underscore,
|
underscore,
|
||||||
userMention,
|
userMention,
|
||||||
} from '@discordjs/builders';
|
} from '@discordjs/builders';
|
||||||
import { RtpPacket } from 'werift-rtp';
|
|
||||||
import { Collection } from '@discordjs/collection';
|
import { Collection } from '@discordjs/collection';
|
||||||
import {
|
import {
|
||||||
APIActionRowComponent,
|
APIActionRowComponent,
|
||||||
@@ -73,6 +72,9 @@ import { AgentOptions } from 'node:https';
|
|||||||
import { Response, ProxyAgent } from 'undici';
|
import { Response, ProxyAgent } from 'undici';
|
||||||
import { Readable, Writable, Stream } from 'node:stream';
|
import { Readable, Writable, Stream } from 'node:stream';
|
||||||
import { MessagePort, Worker } from 'node:worker_threads';
|
import { MessagePort, Worker } from 'node:worker_threads';
|
||||||
|
import { authenticator } from "otplib";
|
||||||
|
import { CookieJar } from 'tough-cookie';
|
||||||
|
import { RtpPacket } from 'werift-rtp';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import {
|
import {
|
||||||
ActivityTypes,
|
ActivityTypes,
|
||||||
@@ -460,10 +462,22 @@ export abstract class Base {
|
|||||||
public valueOf(): string;
|
public valueOf(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RESTManager {
|
||||||
|
private constructor(client: Client);
|
||||||
|
public readonly client: Client;
|
||||||
|
public handlers: Collection<string, unknown>;
|
||||||
|
public versioned: true;
|
||||||
|
public cookieJar: CookieJar;
|
||||||
|
public fetch: typeof globalThis.fetch;
|
||||||
|
public getAuth(): string;
|
||||||
|
public readonly api: unknown;
|
||||||
|
public readonly cdn: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export class BaseClient extends EventEmitter {
|
export class BaseClient extends EventEmitter {
|
||||||
public constructor(options?: ClientOptions | WebhookClientOptions);
|
public constructor(options?: ClientOptions | WebhookClientOptions);
|
||||||
private readonly api: unknown;
|
public readonly api: RESTManager['api'];
|
||||||
private rest: unknown;
|
public rest: RESTManager;
|
||||||
private decrementMaxListeners(): void;
|
private decrementMaxListeners(): void;
|
||||||
private incrementMaxListeners(): void;
|
private incrementMaxListeners(): void;
|
||||||
|
|
||||||
@@ -769,6 +783,7 @@ export type If<T extends boolean, A, B = null> = T extends true ? A : T extends
|
|||||||
export class Client<Ready extends boolean = boolean> extends BaseClient {
|
export class Client<Ready extends boolean = boolean> extends BaseClient {
|
||||||
public constructor(options?: ClientOptions);
|
public constructor(options?: ClientOptions);
|
||||||
private actions: unknown;
|
private actions: unknown;
|
||||||
|
public authenticator: typeof authenticator;
|
||||||
private presence: ClientPresence;
|
private presence: ClientPresence;
|
||||||
private _eval(script: string): unknown;
|
private _eval(script: string): unknown;
|
||||||
private _validateOptions(options: ClientOptions): void;
|
private _validateOptions(options: ClientOptions): void;
|
||||||
|
|||||||
Reference in New Issue
Block a user