From 546c4b91a0e9f7359a8573bba1634384e79f41c8 Mon Sep 17 00:00:00 2001 From: PflaeginGmbh <104005569+PflaeginGmbh@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:37:12 +0100 Subject: [PATCH] Change authentication to apples new SRP6a (#49) --- icloudpy/base.py | 72 ++++++++++++++++++++++++++++++++++++++------ requirements.txt | 1 + tests/__init__.py | 5 ++- tests/const_login.py | 7 +++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/icloudpy/base.py b/icloudpy/base.py index 003cdf2..4a6842a 100644 --- a/icloudpy/base.py +++ b/icloudpy/base.py @@ -8,6 +8,9 @@ from re import match from tempfile import gettempdir from uuid import uuid1 +import srp +import base64 +import hashlib from requests import Session from six import PY2 @@ -319,13 +322,6 @@ def authenticate(self, force_refresh=False, service=None): if not login_successful: LOGGER.debug("Authenticating as %s", self.user["accountName"]) - data = dict(self.user) - - data["rememberMe"] = True - data["trustTokens"] = [] - if self.session_data.get("trust_token"): - data["trustTokens"] = [self.session_data.get("trust_token")] - headers = self._get_auth_headers() if self.session_data.get("scnt"): @@ -334,9 +330,67 @@ def authenticate(self, force_refresh=False, service=None): if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + class SrpPassword(): + def __init__(self, password: str): + self.password = password + + def set_encrypt_info(self, salt: bytes, iterations: int, key_length: int): + self.salt = salt + self.iterations = iterations + self.key_length = key_length + + def encode(self): + password_hash = hashlib.sha256(self.password.encode('utf-8')).digest() + return hashlib.pbkdf2_hmac('sha256', password_hash, salt, iterations, key_length) + + srp_password = SrpPassword(self.user["password"]) + srp.rfc5054_enable() + srp.no_username_in_x() + usr = srp.User(self.user["accountName"], srp_password, hash_alg=srp.SHA256, ng_type=srp.NG_2048) + + uname, A = usr.start_authentication() + + data = { + 'a': base64.b64encode(A).decode(), + 'accountName': uname, + 'protocols': ['s2k', 's2k_fo'] + } + + try: + response = self.session.post(f"{self.auth_endpoint}/signin/init", data=json.dumps(data), + headers=headers) + response.raise_for_status() + except ICloudPyAPIResponseException as error: + msg = "Failed to initiate srp authentication." + raise ICloudPyFailedLoginException(msg, error) from error + + body = response.json() + + salt = base64.b64decode(body['salt']) + b = base64.b64decode(body['b']) + c = body['c'] + iterations = body['iteration'] + key_length = 32 + srp_password.set_encrypt_info(salt, iterations, key_length) + + m1 = usr.process_challenge(salt, b) + m2 = usr.H_AMK + + data = { + "accountName": uname, + "c": c, + "m1": base64.b64encode(m1).decode(), + "m2": base64.b64encode(m2).decode(), + "rememberMe": True, + "trustTokens": [], + } + + if self.session_data.get("trust_token"): + data["trustTokens"] = [self.session_data.get("trust_token")] + try: self.session.post( - f"{self.auth_endpoint}/signin", + f"{self.auth_endpoint}/signin/complete", params={"isRememberMeEnabled": "true"}, data=json.dumps(data), headers=headers, @@ -400,7 +454,7 @@ def _validate_token(self): def _get_auth_headers(self, overrides=None): headers = { - "Accept": "*/*", + "Accept": "application/json, text/javascript", "Content-Type": "application/json", "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", "X-Apple-OAuth-Client-Type": "firstPartyAuth", diff --git a/requirements.txt b/requirements.txt index e8b0645..63d4c76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ keyring==23.11.0 keyrings.alt==4.2.0 click==8.1.7 six==1.16.0 +srp==1.0.21 tzlocal==5.2 pytz==2024.2 certifi==2024.8.30 diff --git a/tests/__init__.py b/tests/__init__.py index ba2e540..1d26667 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -31,6 +31,7 @@ from .const_findmyiphone import FMI_FAMILY_WORKING from .const_login import ( AUTH_OK, + SRP_INIT_OK, LOGIN_2FA, LOGIN_WORKING, TRUSTED_DEVICE_1, @@ -102,9 +103,11 @@ def request(self, method, url, **kwargs): if "signin" in url and method == "POST": if ( data.get("accountName") not in VALID_USERS - or data.get("password") != VALID_PASSWORD + # or data.get("password") != VALID_PASSWORD ): self._raise_error(None, "Unknown reason") + if url.endswith('/init'): + return ResponseMock(SRP_INIT_OK) if data.get("accountName") == REQUIRES_2FA_USER: self.service.session_data["session_token"] = REQUIRES_2FA_TOKEN return ResponseMock(AUTH_OK) diff --git a/tests/const_login.py b/tests/const_login.py index 6258bdb..8327f02 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -16,6 +16,13 @@ # Data AUTH_OK = {"authType": "hsa2"} +SRP_INIT_OK = {'iteration': 20433, + 'salt': '0samK84bcBmkVsswOpZbZg==', + 'protocol': 's2k', + 'b': 'STVHcWTN9YOYn4IgtIJ6UPdPbvzvL+zza/l+6yUHUtdEyxwzpB78y8wqZ8QWSbVqjBcpl32iEA4T3nYp0LWZ5hD3r3yIJFloXvX0kpBJkr+Nh8EfHuW1V50A8riH6VWyuJ8m3JmOO7/xkNgP7je8GMpt/5f/7qE3AOj73e3JR0fzQ7IopdU0tlyVX0tD7T6wCyHS52GJWDdq1I2bgzurIK2/ZjR/Hwzd/67oFQPtKQgjrSRaKo5MJEfDP7C9wOlXsZqbb7igX6PeZRWrfl+iQFaA/FVeWSngB07ja3wOryY9GsYO06ELGOaQ+MpsT7mouqrGTfOJ0OMh9EgrkJEM6w==', + 'c': 'e-1be-8746c235-b41c-11ef-bd17-c780acb4fe15:PRN' + } + LOGIN_WORKING = { "dsInfo": { "lastName": LAST_NAME,