feat(client): control 2fa password

This commit is contained in:
teidesu 2021-05-09 14:35:47 +03:00
parent 320f4fdd24
commit 192c0f773e
5 changed files with 214 additions and 15 deletions

View file

@ -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<void> {
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
}
})
}

View file

@ -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<void> {
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
}
})
}

View file

@ -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<void> {
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<void> {
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<void> {
await this.call({
_: 'account.cancelPasswordEmail'
})
}

View file

@ -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<void> {
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: ''
}
})
}

View file

@ -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<Buffer> {
// 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<Buffer> {
(algo as tl.Mutable<typeof algo>).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)