ソースを参照

Add 2FA helper method

painor 3 年 前
コミット
7cb3000196

+ 140 - 0
gramjs/client/2fa.ts

@@ -0,0 +1,140 @@
+import { generateRandomBytes } from "../Helpers";
+import { computeCheck, computeDigest } from "../Password";
+import type { TelegramClient } from "./TelegramClient";
+import { Api } from "../tl";
+import { errors } from "../index";
+
+export interface TwoFaParams {
+    isCheckPassword?: boolean;
+    currentPassword?: string;
+    newPassword?: string;
+    hint?: string;
+    email?: string;
+    emailCodeCallback?: (length: number) => Promise<string>;
+    onEmailCodeError?: (err: Error) => void;
+}
+
+/**
+ * Changes the 2FA settings of the logged in user.
+ Note that this method may be *incredibly* slow depending on the
+ prime numbers that must be used during the process to make sure
+ that everything is safe.
+
+ Has no effect if both current and new password are omitted.
+
+ * @param client: The telegram client instance
+ * @param isCheckPassword: Must be ``true`` if you want to check the current password
+ * @param currentPassword: The current password, to authorize changing to ``new_password``.
+ Must be set if changing existing 2FA settings.
+ Must **not** be set if 2FA is currently disabled.
+ Passing this by itself will remove 2FA (if correct).
+ * @param newPassword: The password to set as 2FA.
+ If 2FA was already enabled, ``currentPassword`` **must** be set.
+ Leaving this blank or `undefined` will remove the password.
+ * @param hint: Hint to be displayed by Telegram when it asks for 2FA.
+ Must be set when changing or creating a new password.
+ Has no effect if ``newPassword`` is not set.
+ * @param email: Recovery and verification email. If present, you must also
+ set `emailCodeCallback`, else it raises an Error.
+ * @param emailCodeCallback: If an email is provided, a callback that returns the code sent
+ to it must also be set. This callback may be asynchronous.
+ It should return a string with the code. The length of the
+ code will be passed to the callback as an input parameter.
+ * @param onEmailCodeError: Called when an error happens while sending an email.
+
+ If the callback returns an invalid code, it will raise an rpc error with the message
+ ``CODE_INVALID``
+
+ * @returns Promise<void>
+ * @throws this method can throw:
+ "PASSWORD_HASH_INVALID" if you entered a wrong password (or set it to undefined).
+ "EMAIL_INVALID" if the entered email is wrong
+ "EMAIL_HASH_EXPIRED" if the user took too long to verify their email
+ */
+export async function updateTwoFaSettings(
+    client: TelegramClient,
+    {
+        isCheckPassword,
+        currentPassword,
+        newPassword,
+        hint = "",
+        email,
+        emailCodeCallback,
+        onEmailCodeError,
+    }: TwoFaParams
+) {
+    if (!newPassword && !currentPassword) {
+        throw new Error(
+            "Neither `currentPassword` nor `newPassword` is present"
+        );
+    }
+
+    if (email && !(emailCodeCallback && onEmailCodeError)) {
+        throw new Error(
+            "`email` present without `emailCodeCallback` and `onEmailCodeError`"
+        );
+    }
+
+    const pwd = await client.invoke(new Api.account.GetPassword());
+
+    if (!(pwd.newAlgo instanceof Api.PasswordKdfAlgoUnknown)) {
+        pwd.newAlgo.salt1 = Buffer.concat([
+            pwd.newAlgo.salt1,
+            generateRandomBytes(32),
+        ]);
+    }
+    if (!pwd.hasPassword && currentPassword) {
+        currentPassword = undefined;
+    }
+
+    const password = currentPassword
+        ? await computeCheck(pwd, currentPassword!)
+        : new Api.InputCheckPasswordEmpty();
+
+    if (isCheckPassword) {
+        await client.invoke(new Api.auth.CheckPassword({ password }));
+        return;
+    }
+    if (pwd.newAlgo instanceof Api.PasswordKdfAlgoUnknown) {
+        throw new Error("Unknown password encryption method");
+    }
+    try {
+        await client.invoke(
+            new Api.account.UpdatePasswordSettings({
+                password,
+                newSettings: new Api.account.PasswordInputSettings({
+                    newAlgo: pwd.newAlgo,
+                    newPasswordHash: newPassword
+                        ? await computeDigest(pwd.newAlgo, newPassword)
+                        : Buffer.alloc(0),
+                    hint,
+                    email,
+                    // not explained what it does and it seems to always be set to empty in tdesktop
+                    newSecureSettings: undefined,
+                }),
+            })
+        );
+    } catch (e) {
+        if (e instanceof errors.EmailUnconfirmedError) {
+            // eslint-disable-next-line no-constant-condition
+            while (true) {
+                try {
+                    const code = await emailCodeCallback!(e.codeLength);
+
+                    if (!code) {
+                        throw new Error("Code is empty");
+                    }
+
+                    await client.invoke(
+                        new Api.account.ConfirmPasswordEmail({ code })
+                    );
+                    break;
+                } catch (err) {
+                    onEmailCodeError!(err);
+                }
+            }
+        } else {
+            throw e;
+        }
+    }
+}

+ 58 - 0
gramjs/client/TelegramClient.ts

@@ -11,6 +11,7 @@ import * as uploadMethods from "./uploads";
 import * as userMethods from "./users";
 import * as chatMethods from "./chats";
 import * as dialogMethods from "./dialogs";
+import * as twoFA from "./2fa";
 import type { ButtonLike, Entity, EntityLike, MessageIDLike } from "../define";
 import { Api } from "../tl";
 import { sanitizeParseMode } from "../Utils";
@@ -251,6 +252,63 @@ export class TelegramClient extends TelegramBaseClient {
         return authMethods.signInBot(this, apiCredentials, authParams);
     }
 
+    /**
+     * Changes the 2FA settings of the logged in user.
+     Note that this method may be *incredibly* slow depending on the
+     prime numbers that must be used during the process to make sure
+     that everything is safe.
+
+     Has no effect if both current and new password are omitted.
+
+     * @param client: The telegram client instance
+     * @param isCheckPassword: Must be ``true`` if you want to check the current password
+     * @param currentPassword: The current password, to authorize changing to ``new_password``.
+     Must be set if changing existing 2FA settings.
+     Must **not** be set if 2FA is currently disabled.
+     Passing this by itself will remove 2FA (if correct).
+     * @param newPassword: The password to set as 2FA.
+     If 2FA was already enabled, ``currentPassword`` **must** be set.
+     Leaving this blank or `undefined` will remove the password.
+     * @param hint: Hint to be displayed by Telegram when it asks for 2FA.
+     Must be set when changing or creating a new password.
+     Has no effect if ``newPassword`` is not set.
+     * @param email: Recovery and verification email. If present, you must also
+     set `emailCodeCallback`, else it raises an Error.
+     * @param emailCodeCallback: If an email is provided, a callback that returns the code sent
+     to it must also be set. This callback may be asynchronous.
+     It should return a string with the code. The length of the
+     code will be passed to the callback as an input parameter.
+     * @param onEmailCodeError: Called when an error happens while sending an email.
+
+     If the callback returns an invalid code, it will raise an rpc error with the message
+     ``CODE_INVALID``
+
+     * @returns Promise<void>
+     * @throws this method can throw:
+     "PASSWORD_HASH_INVALID" if you entered a wrong password (or set it to undefined).
+     "EMAIL_INVALID" if the entered email is wrong
+     "EMAIL_HASH_EXPIRED" if the user took too long to verify their email
+     */
+    async updateTwoFaSettings({
+        isCheckPassword,
+        currentPassword,
+        newPassword,
+        hint = "",
+        email,
+        emailCodeCallback,
+        onEmailCodeError,
+    }: twoFA.TwoFaParams) {
+        return twoFA.updateTwoFaSettings(this, {
+            isCheckPassword,
+            currentPassword,
+            newPassword,
+            hint,
+            email,
+            emailCodeCallback,
+            onEmailCodeError,
+        });
+    }
+
     //endregion auth
 
     //region bot

+ 40 - 1
gramjs/errors/RPCErrorList.ts

@@ -1,4 +1,9 @@
-import { RPCError, InvalidDCError, FloodError } from "./RPCBaseErrors";
+import {
+    RPCError,
+    InvalidDCError,
+    FloodError,
+    BadRequestError,
+} from "./RPCBaseErrors";
 
 export class UserMigrateError extends InvalidDCError {
     public newDc: number;
@@ -119,12 +124,46 @@ export class NetworkMigrateError extends InvalidDCError {
     }
 }
 
+export class EmailUnconfirmedError extends BadRequestError {
+    codeLength: number;
+
+    constructor(args: any) {
+        const codeLength = Number(args.capture || 0);
+        super(
+            `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
+                args.request
+            )}`,
+            args.request,
+            400
+        );
+        // eslint-disable-next-line max-len
+        this.message = `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
+            args.request
+        )}`;
+        this.codeLength = codeLength;
+    }
+}
+
+export class MsgWaitError extends FloodError {
+    constructor(args: any) {
+        super(
+            `Message failed to be sent.${RPCError._fmtRequest(args.request)}`,
+            args.request
+        );
+        this.message = `Message failed to be sent.${RPCError._fmtRequest(
+            args.request
+        )}`;
+    }
+}
+
 export const rpcErrorRe = new Map<RegExp, any>([
     [/FILE_MIGRATE_(\d+)/, FileMigrateError],
     [/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError],
     [/FLOOD_WAIT_(\d+)/, FloodWaitError],
+    [/MSG_WAIT_(.*)/, MsgWaitError],
     [/PHONE_MIGRATE_(\d+)/, PhoneMigrateError],
     [/SLOWMODE_WAIT_(\d+)/, SlowModeWaitError],
     [/USER_MIGRATE_(\d+)/, UserMigrateError],
     [/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
+    [/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
 ]);

+ 2 - 1
gramjs/tl/custom/message.ts

@@ -799,7 +799,8 @@ export class Message extends Mixin(SenderGetter, ChatGetter) {
 
     async downloadMedia(params: DownloadMediaInterface) {
         // small hack for patched method
-        if (this._client) return this._client.downloadMedia(this as any, params);
+        if (this._client)
+            return this._client.downloadMedia(this as any, params);
     }
 
     /* TODO doesn't look good enough.

+ 0 - 1
gramjs/tl/generationHelpers.ts

@@ -245,7 +245,6 @@ const parseTl = function* (
     // Once all objects have been parsed, replace the
     // string type from the arguments with references
     for (const obj of objAll) {
-
         if (AUTH_KEY_TYPES.has(obj.constructorId)) {
             for (const arg in obj.argsConfig) {
                 if (obj.argsConfig[arg].type === "string") {