feat(client): control 2fa password
This commit is contained in:
parent
320f4fdd24
commit
192c0f773e
5 changed files with 214 additions and 15 deletions
44
packages/client/src/methods/pasword/change-cloud-password.ts
Normal file
44
packages/client/src/methods/pasword/change-cloud-password.ts
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
49
packages/client/src/methods/pasword/enable-cloud-password.ts
Normal file
49
packages/client/src/methods/pasword/enable-cloud-password.ts
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
39
packages/client/src/methods/pasword/password-email.ts
Normal file
39
packages/client/src/methods/pasword/password-email.ts
Normal 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'
|
||||
})
|
||||
}
|
31
packages/client/src/methods/pasword/remove-cloud-password.ts
Normal file
31
packages/client/src/methods/pasword/remove-cloud-password.ts
Normal 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: ''
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue