diff --git a/pyrogram/client/methods/password/change_cloud_password.py b/pyrogram/client/methods/password/change_cloud_password.py index 6a65d51b..73775056 100644 --- a/pyrogram/client/methods/password/change_cloud_password.py +++ b/pyrogram/client/methods/password/change_cloud_password.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import os + +from pyrogram.api import functions, types +from .utils import compute_hash, compute_check, btoi, itob from ...ext import BaseClient @@ -37,33 +41,30 @@ class ChangeCloudPassword(BaseClient): A new password hint. Returns: - True on success, False otherwise. + True on success. Raises: :class:`Error ` in case of a Telegram RPC error. + ``ValueError`` in case there is no cloud password to change. """ - raise NotImplementedError( - "Cloud password methods are currently not available. " - "See https://github.com/pyrogram/pyrogram/issues/178" + r = self.send(functions.account.GetPassword()) + + if not r.has_password: + raise ValueError("There is no cloud password to change") + + r.new_algo.salt1 += os.urandom(32) + new_hash = btoi(compute_hash(r.new_algo, new_password)) + new_hash = itob(pow(r.new_algo.g, new_hash, btoi(r.new_algo.p))) + + self.send( + functions.account.UpdatePasswordSettings( + password=compute_check(r, current_password), + new_settings=types.account.PasswordInputSettings( + new_algo=r.new_algo, + new_password_hash=new_hash, + hint=new_hint + ) + ) ) - # r = self.send(functions.account.GetPassword()) - # - # if isinstance(r, types.account.Password): - # current_password_hash = sha256(r.current_salt + current_password.encode() + r.current_salt).digest() - # - # new_salt = r.new_salt + os.urandom(8) - # new_password_hash = sha256(new_salt + new_password.encode() + new_salt).digest() - # - # return self.send( - # functions.account.UpdatePasswordSettings( - # current_password_hash=current_password_hash, - # new_settings=types.account.PasswordInputSettings( - # new_salt=new_salt, - # new_password_hash=new_password_hash, - # hint=new_hint - # ) - # ) - # ) - # else: - # return False + return True diff --git a/pyrogram/client/methods/password/enable_cloud_password.py b/pyrogram/client/methods/password/enable_cloud_password.py index 28542815..c8fd4dcb 100644 --- a/pyrogram/client/methods/password/enable_cloud_password.py +++ b/pyrogram/client/methods/password/enable_cloud_password.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import os + +from pyrogram.api import functions, types +from .utils import compute_hash, btoi, itob from ...ext import BaseClient @@ -23,10 +27,10 @@ class EnableCloudPassword(BaseClient): def enable_cloud_password(self, password: str, hint: str = "", - email: str = "") -> bool: + email: str = None) -> bool: """Use this method to enable the Two-Step Verification security feature (Cloud Password) on your account. - This password will be asked when you log in on a new device in addition to the SMS code. + This password will be asked when you log-in on a new device in addition to the SMS code. Args: password (``str``): @@ -39,32 +43,31 @@ class EnableCloudPassword(BaseClient): Recovery e-mail. Returns: - True on success, False otherwise. + True on success. Raises: :class:`Error ` in case of a Telegram RPC error. + ``ValueError`` in case there is already a cloud password enabled. """ - raise NotImplementedError( - "Cloud password methods are currently not available. " - "See https://github.com/pyrogram/pyrogram/issues/178" + r = self.send(functions.account.GetPassword()) + + if r.has_password: + raise ValueError("There is already a cloud password enabled") + + r.new_algo.salt1 += os.urandom(32) + new_hash = btoi(compute_hash(r.new_algo, password)) + new_hash = itob(pow(r.new_algo.g, new_hash, btoi(r.new_algo.p))) + + self.send( + functions.account.UpdatePasswordSettings( + password=types.InputCheckPasswordEmpty(), + new_settings=types.account.PasswordInputSettings( + new_algo=r.new_algo, + new_password_hash=new_hash, + hint=hint, + email=email + ) + ) ) - # r = self.send(functions.account.GetPassword()) - # - # if isinstance(r, types.account.NoPassword): - # salt = r.new_salt + os.urandom(8) - # password_hash = sha256(salt + password.encode() + salt).digest() - # - # return self.send( - # functions.account.UpdatePasswordSettings( - # current_password_hash=salt, - # new_settings=types.account.PasswordInputSettings( - # new_salt=salt, - # new_password_hash=password_hash, - # hint=hint, - # email=email - # ) - # ) - # ) - # else: - # return False + return True diff --git a/pyrogram/client/methods/password/remove_cloud_password.py b/pyrogram/client/methods/password/remove_cloud_password.py index 9b45fc8e..a86ed5f8 100644 --- a/pyrogram/client/methods/password/remove_cloud_password.py +++ b/pyrogram/client/methods/password/remove_cloud_password.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from pyrogram.api import functions, types +from .utils import compute_check from ...ext import BaseClient @@ -29,30 +31,26 @@ class RemoveCloudPassword(BaseClient): Your current password. Returns: - True on success, False otherwise. + True on success. Raises: :class:`Error ` in case of a Telegram RPC error. + ``ValueError`` in case there is no cloud password to remove. """ - raise NotImplementedError( - "Cloud password methods are currently not available. " - "See https://github.com/pyrogram/pyrogram/issues/178" + r = self.send(functions.account.GetPassword()) + + if not r.has_password: + raise ValueError("There is no cloud password to remove") + + self.send( + functions.account.UpdatePasswordSettings( + password=compute_check(r, password), + new_settings=types.account.PasswordInputSettings( + new_algo=types.PasswordKdfAlgoUnknown(), + new_password_hash=b"", + hint="" + ) + ) ) - # r = self.send(functions.account.GetPassword()) - # - # if isinstance(r, types.account.Password): - # password_hash = sha256(r.current_salt + password.encode() + r.current_salt).digest() - # - # return self.send( - # functions.account.UpdatePasswordSettings( - # current_password_hash=password_hash, - # new_settings=types.account.PasswordInputSettings( - # new_salt=b"", - # new_password_hash=b"", - # hint="" - # ) - # ) - # ) - # else: - # return False + return True diff --git a/pyrogram/client/methods/password/utils.py b/pyrogram/client/methods/password/utils.py new file mode 100644 index 00000000..4bf0ddec --- /dev/null +++ b/pyrogram/client/methods/password/utils.py @@ -0,0 +1,103 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import hashlib +import os + +from pyrogram.api import types + + +def btoi(b: bytes) -> int: + return int.from_bytes(b, "big") + + +def itob(i: int) -> bytes: + return i.to_bytes(256, "big") + + +def sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def xor(a: bytes, b: bytes) -> bytes: + return bytes(i ^ j for i, j in zip(a, b)) + + +def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: str) -> bytes: + hash1 = sha256(algo.salt1 + password.encode() + algo.salt1) + hash2 = sha256(algo.salt2 + hash1 + algo.salt2) + hash3 = hashlib.pbkdf2_hmac("sha512", hash2, algo.salt1, 100000) + + return sha256(algo.salt2 + hash3 + algo.salt2) + + +def compute_check(r: types.account.Password, password: str) -> types.InputCheckPasswordSRP: + algo = r.current_algo + + p_bytes = algo.p + p = btoi(algo.p) + + g_bytes = itob(algo.g) + g = algo.g + + B_bytes = r.srp_B + B = btoi(B_bytes) + + srp_id = r.srp_id + + x_bytes = compute_hash(algo, password) + x = btoi(x_bytes) + + g_x = pow(g, x, p) + + k_bytes = sha256(p_bytes + g_bytes) + k = btoi(k_bytes) + + kg_x = (k * g_x) % p + + while True: + a_bytes = os.urandom(256) + a = btoi(a_bytes) + + A = pow(g, a, p) + A_bytes = itob(A) + + u = btoi(sha256(A_bytes + B_bytes)) + + if u > 0: + break + + g_b = (B - kg_x) % p + + ux = u * x + a_ux = a + ux + S = pow(g_b, a_ux, p) + S_bytes = itob(S) + + K_bytes = sha256(S_bytes) + + M1_bytes = sha256( + xor(sha256(p_bytes), sha256(g_bytes)) + + sha256(algo.salt1) + + sha256(algo.salt2) + + A_bytes + + B_bytes + + K_bytes + ) + + return types.InputCheckPasswordSRP(srp_id, A_bytes, M1_bytes)