From 192c0f773e9aa23967711d59e59ee1803c240855 Mon Sep 17 00:00:00 2001 From: teidesu Date: Sun, 9 May 2021 14:35:47 +0300 Subject: [PATCH] feat(client): control 2fa password --- .../methods/pasword/change-cloud-password.ts | 44 +++++++++++++ .../methods/pasword/enable-cloud-password.ts | 49 ++++++++++++++ .../src/methods/pasword/password-email.ts | 39 +++++++++++ .../methods/pasword/remove-cloud-password.ts | 31 +++++++++ packages/core/src/utils/crypto/password.ts | 66 ++++++++++++++----- 5 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 packages/client/src/methods/pasword/change-cloud-password.ts create mode 100644 packages/client/src/methods/pasword/enable-cloud-password.ts create mode 100644 packages/client/src/methods/pasword/password-email.ts create mode 100644 packages/client/src/methods/pasword/remove-cloud-password.ts diff --git a/packages/client/src/methods/pasword/change-cloud-password.ts b/packages/client/src/methods/pasword/change-cloud-password.ts new file mode 100644 index 00000000..e89b8943 --- /dev/null +++ b/packages/client/src/methods/pasword/change-cloud-password.ts @@ -0,0 +1,44 @@ +import { TelegramClient } from '../../client' +import { MtCuteArgumentError } from '../../types' +import { assertTypeIs } from '../../utils/type-assertion' +import { computeSrpParams, computeNewPasswordHash } from '@mtcute/core' + +/** + * Change your 2FA password + * + * @param currentPassword Current password as plaintext + * @param newPassword New password as plaintext + * @param hint Hint for the new password + * @internal + */ +export async function changeCloudPassword( + this: TelegramClient, + currentPassword: string, + newPassword: string, + hint?: string +): Promise { + const pwd = await this.call({ _: 'account.getPassword' }) + if (!pwd.hasPassword) + throw new MtCuteArgumentError('Cloud password is not enabled') + + const algo = pwd.newAlgo + assertTypeIs( + 'account.getPassword', + algo, + 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow' + ) + + const oldSrp = await computeSrpParams(this._crypto, pwd, currentPassword) + const newHash = await computeNewPasswordHash(this._crypto, algo, newPassword) + + await this.call({ + _: 'account.updatePasswordSettings', + password: oldSrp, + newSettings: { + _: 'account.passwordInputSettings', + newAlgo: algo, + newPasswordHash: newHash, + hint + } + }) +} diff --git a/packages/client/src/methods/pasword/enable-cloud-password.ts b/packages/client/src/methods/pasword/enable-cloud-password.ts new file mode 100644 index 00000000..452d3442 --- /dev/null +++ b/packages/client/src/methods/pasword/enable-cloud-password.ts @@ -0,0 +1,49 @@ +import { TelegramClient } from '../../client' +import { MtCuteArgumentError } from '../../types' +import { assertTypeIs } from '../../utils/type-assertion' +import { computeNewPasswordHash } from '@mtcute/core' + +/** + * Enable 2FA password on your account + * + * Note that if you pass `email`, `EmailUnconfirmedError` may be + * thrown, and you should use {@link verifyPasswordEmail}, + * {@link resendPasswordEmail} or {@link cancelPasswordEmail}, + * and the call this method again + * + * @param password 2FA password as plaintext + * @param hint Hint for the new password + * @param email Recovery email + * @internal + */ +export async function enableCloudPassword( + this: TelegramClient, + password: string, + hint?: string, + email?: string +): Promise { + const pwd = await this.call({ _: 'account.getPassword' }) + if (pwd.hasPassword) + throw new MtCuteArgumentError('Cloud password is already enabled') + + const algo = pwd.newAlgo + assertTypeIs( + 'account.getPassword', + algo, + 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow' + ) + + const newHash = await computeNewPasswordHash(this._crypto, algo, password) + + await this.call({ + _: 'account.updatePasswordSettings', + password: { _: 'inputCheckPasswordEmpty' }, + newSettings: { + _: 'account.passwordInputSettings', + newAlgo: algo, + newPasswordHash: newHash, + hint, + email + } + }) +} diff --git a/packages/client/src/methods/pasword/password-email.ts b/packages/client/src/methods/pasword/password-email.ts new file mode 100644 index 00000000..78e50901 --- /dev/null +++ b/packages/client/src/methods/pasword/password-email.ts @@ -0,0 +1,39 @@ +import { TelegramClient } from '../../client' + +/** + * Verify an email to use as 2FA recovery method + * + * @param code Code which was sent via email + * @internal + */ +export async function verifyPasswordEmail( + this: TelegramClient, + code: string +): Promise { + await this.call({ + _: 'account.confirmPasswordEmail', + code + }) +} + +/** + * Resend the code to verify an email to use as 2FA recovery method. + * + * @internal + */ +export async function resendPasswordEmail(this: TelegramClient): Promise { + await this.call({ + _: 'account.resendPasswordEmail' + }) +} + +/** + * Cancel the code that was sent to verify an email to use as 2FA recovery method + * + * @internal + */ +export async function cancelPasswordEmail(this: TelegramClient): Promise { + await this.call({ + _: 'account.cancelPasswordEmail' + }) +} diff --git a/packages/client/src/methods/pasword/remove-cloud-password.ts b/packages/client/src/methods/pasword/remove-cloud-password.ts new file mode 100644 index 00000000..0ebbb1d6 --- /dev/null +++ b/packages/client/src/methods/pasword/remove-cloud-password.ts @@ -0,0 +1,31 @@ +import { TelegramClient } from '../../client' +import { MtCuteArgumentError } from '../../types' +import { computeSrpParams } from '@mtcute/core' + +/** + * Remove 2FA password from your account + * + * @param password 2FA password as plaintext + * @internal + */ +export async function removeCloudPassword( + this: TelegramClient, + password: string, +): Promise { + const pwd = await this.call({ _: 'account.getPassword' }) + if (!pwd.hasPassword) + throw new MtCuteArgumentError('Cloud password is not enabled') + + const oldSrp = await computeSrpParams(this._crypto, pwd, password) + + await this.call({ + _: 'account.updatePasswordSettings', + password: oldSrp, + newSettings: { + _: 'account.passwordInputSettings', + newAlgo: { _: 'passwordKdfAlgoUnknown' }, + newPasswordHash: Buffer.alloc(0), + hint: '' + } + }) +} diff --git a/packages/core/src/utils/crypto/password.ts b/packages/core/src/utils/crypto/password.ts index ca09bb22..32fab2b5 100644 --- a/packages/core/src/utils/crypto/password.ts +++ b/packages/core/src/utils/crypto/password.ts @@ -4,6 +4,47 @@ import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils' import bigInt from 'big-integer' import { randomBytes, xorBuffer } from '../buffer-utils' +export async function computePasswordHash( + crypto: ICryptoProvider, + password: Buffer, + salt1: Buffer, + salt2: Buffer +): Promise { + // https://core.telegram.org/api/srp#checking-the-password-with-srp + const SH = (data: Buffer, salt: Buffer) => + crypto.sha256(Buffer.concat([salt, data, salt])) + const PH1 = async (pwd: Buffer, salt1: Buffer, salt2: Buffer) => + SH(await SH(pwd, salt1), salt2) + const PH2 = async (pwd: Buffer, salt1: Buffer, salt2: Buffer) => + SH( + await crypto.pbkdf2(await PH1(pwd, salt1, salt2), salt1, 100000), + salt2 + ) + + return PH2(password, salt1, salt2) +} + +export async function computeNewPasswordHash( + crypto: ICryptoProvider, + algo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, + password: string +): Promise { + (algo as tl.Mutable).salt1 = Buffer.concat([algo.salt1, randomBytes(32)]) + + const _x = await computePasswordHash( + crypto, + Buffer.from(password), + algo.salt1, + algo.salt2 + ) + + const g = bigInt(algo.g) + const p = bufferToBigInt(algo.p) + const x = bufferToBigInt(_x) + + return bigIntToBuffer(g.modPow(x, p), 256) +} + export async function computeSrpParams( crypto: ICryptoProvider, request: tl.account.RawPassword, @@ -20,18 +61,6 @@ export async function computeSrpParams( const algo = request.currentAlgo - // https://core.telegram.org/api/srp#checking-the-password-with-srp - const H = (data: Buffer) => crypto.sha256(data) - const SH = (data: Buffer, salt: Buffer) => - H(Buffer.concat([salt, data, salt])) - const PH1 = async (pwd: Buffer, salt1: Buffer, salt2: Buffer) => - SH(await SH(pwd, salt1), salt2) - const PH2 = async (pwd: Buffer, salt1: Buffer, salt2: Buffer) => - SH( - await crypto.pbkdf2(await PH1(pwd, salt1, salt2), salt1, 100000), - salt2 - ) - // here and after: underscored variables are buffers, non-underscored are bigInts const g = bigInt(algo.g) @@ -43,11 +72,18 @@ export async function computeSrpParams( const gA = g.modPow(a, p) const _gA = bigIntToBuffer(gA, 256) + const H = (data: Buffer) => crypto.sha256(data) + const [_k, _u, _x] = await Promise.all([ // maybe, just maybe this will be a bit faster with some crypto providers - /* k = */ H(Buffer.concat([algo.p, _g])), - /* u = */ H(Buffer.concat([_gA, request.srpB!])), - /* x = */ PH2(Buffer.from(password), algo.salt1, algo.salt2), + /* k = */ crypto.sha256(Buffer.concat([algo.p, _g])), + /* u = */ crypto.sha256(Buffer.concat([_gA, request.srpB!])), + /* x = */ computePasswordHash( + crypto, + Buffer.from(password), + algo.salt1, + algo.salt2 + ), ]) const k = bufferToBigInt(_k) const u = bufferToBigInt(_u)