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 bigInt from 'big-integer'
|
||||||
import { randomBytes, xorBuffer } from '../buffer-utils'
|
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(
|
export async function computeSrpParams(
|
||||||
crypto: ICryptoProvider,
|
crypto: ICryptoProvider,
|
||||||
request: tl.account.RawPassword,
|
request: tl.account.RawPassword,
|
||||||
|
@ -20,18 +61,6 @@ export async function computeSrpParams(
|
||||||
|
|
||||||
const algo = request.currentAlgo
|
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
|
// here and after: underscored variables are buffers, non-underscored are bigInts
|
||||||
|
|
||||||
const g = bigInt(algo.g)
|
const g = bigInt(algo.g)
|
||||||
|
@ -43,11 +72,18 @@ export async function computeSrpParams(
|
||||||
const gA = g.modPow(a, p)
|
const gA = g.modPow(a, p)
|
||||||
const _gA = bigIntToBuffer(gA, 256)
|
const _gA = bigIntToBuffer(gA, 256)
|
||||||
|
|
||||||
|
const H = (data: Buffer) => crypto.sha256(data)
|
||||||
|
|
||||||
const [_k, _u, _x] = await Promise.all([
|
const [_k, _u, _x] = await Promise.all([
|
||||||
// maybe, just maybe this will be a bit faster with some crypto providers
|
// maybe, just maybe this will be a bit faster with some crypto providers
|
||||||
/* k = */ H(Buffer.concat([algo.p, _g])),
|
/* k = */ crypto.sha256(Buffer.concat([algo.p, _g])),
|
||||||
/* u = */ H(Buffer.concat([_gA, request.srpB!])),
|
/* u = */ crypto.sha256(Buffer.concat([_gA, request.srpB!])),
|
||||||
/* x = */ PH2(Buffer.from(password), algo.salt1, algo.salt2),
|
/* x = */ computePasswordHash(
|
||||||
|
crypto,
|
||||||
|
Buffer.from(password),
|
||||||
|
algo.salt1,
|
||||||
|
algo.salt2
|
||||||
|
),
|
||||||
])
|
])
|
||||||
const k = bufferToBigInt(_k)
|
const k = bufferToBigInt(_k)
|
||||||
const u = bufferToBigInt(_u)
|
const u = bufferToBigInt(_u)
|
||||||
|
|
Loading…
Reference in a new issue