77from time import time
88from typing import Optional , Tuple
99
10- from .constants import ENROLL_HOSTS , PATHS
10+ from .constants import ENROLL_HOSTS
1111from .crypto import decrypt , encrypt , restore_code_to_bytes
1212from .utils import normalize_serial
1313
@@ -18,27 +18,48 @@ def __init__(self, msg, response):
1818 super ().__init__ (msg )
1919
2020
21- def _post (host : str , path : str , * , data : Optional [str ] = None ) -> bytes :
22- """
23- Send computed data to Blizzard servers
24- Return the answer from the server
25- """
26- conn = HTTPConnection (host )
27- conn .request ("POST" , path , data )
28- response = conn .getresponse ()
21+ class APIClient :
22+ def __init__ (self , * , region : str = "US" , host : str = "" ):
23+ self .region = region
24+ self .host = host or ENROLL_HOSTS .get (region , ENROLL_HOSTS ["default" ])
25+
26+ def post (self , path : str , * , data : Optional [str ] = None ) -> bytes :
27+ conn = HTTPConnection (self .host )
28+ conn .request ("POST" , path , data )
29+ response = conn .getresponse ()
30+
31+ if response .status != 200 :
32+ raise HTTPError ("%s returned status %i" % (self .host , response .status ), response )
2933
30- if response .status != 200 :
31- raise HTTPError ("%s returned status %i" % (host , response .status ), response )
34+ ret = response .read ()
35+ conn .close ()
36+ return ret
3237
33- ret = response .read ()
34- conn .close ()
35- return ret
38+ def enroll (self , data ):
39+ return self .post ("/enrollment/enroll.htm" , data = data )
3640
41+ def get_time (self ) -> int :
42+ response = self .post ("/enrollment/time.htm" )
43+ return int (struct .unpack (">Q" , response )[0 ])
3744
38- def enroll (
39- data : str , host : str = ENROLL_HOSTS ["default" ], path : str = PATHS ["enroll" ]
40- ) -> bytes :
41- return _post (host , path , data = data )
45+ def initiate_paper_restore (self , serial : str ):
46+ response = self .post ("/enrollment/initiatePaperRestore.htm" , data = serial )
47+ resp_size = len (response )
48+ if resp_size != 32 :
49+ raise ValueError ("Bad challenge response (%i bytes)" % (resp_size ))
50+
51+ return response
52+
53+ def validate_paper_restore (self , serial : str , encrypted_data : str ):
54+ data = serial + encrypted_data
55+ try :
56+ response = self .post ("/enrollment/validatePaperRestore.htm" , data = data )
57+ except HTTPError as e :
58+ if e .response .status == 600 :
59+ raise HTTPError ("Invalid serial or restore key" , e .response )
60+ else :
61+ raise
62+ return response
4263
4364
4465def request_new_serial (
@@ -57,14 +78,15 @@ def base_msg(otp, region, model):
5778
5879 otp = token_bytes (37 )
5980 data = base_msg (otp , region , model )
81+ encrypted_data = encrypt (data )
82+
83+ client = APIClient (region = region )
84+ response = client .enroll (encrypted_data )[8 :]
6085
61- e = encrypt (data )
62- # get the host, or fallback to default
63- host = ENROLL_HOSTS .get (region , ENROLL_HOSTS ["default" ])
64- response = decrypt (enroll (e , host )[8 :], otp )
86+ decrypted_response = decrypt (response , otp )
6587
66- secret = b32encode (response [:20 ]).decode ()
67- serial = response [20 :].decode ()
88+ secret = b32encode (decrypted_response [:20 ]).decode ()
89+ serial = decrypted_response [20 :].decode ()
6890
6991 region = serial [:2 ]
7092 if region not in ("CN" , "EU" , "US" ):
@@ -73,7 +95,7 @@ def base_msg(otp, region, model):
7395 return serial , secret
7496
7597
76- def get_time_offset (region : str = "US" , path : str = PATHS [ "time" ] ) -> int :
98+ def get_time_offset (region : str = "US" ) -> int :
7799 """
78100 Calculates the time difference in seconds as a float
79101 between the local host and a remote server
@@ -82,52 +104,30 @@ def get_time_offset(region: str = "US", path: str = PATHS["time"]) -> int:
82104 Negative numbers indicate the local clock is ahead of the
83105 server clock.
84106 """
85- host = ENROLL_HOSTS . get (region , ENROLL_HOSTS [ "default" ] )
86- response = _post ( host , path )
87- t = time ()
107+ client = APIClient (region = region )
108+ server_time = client . get_time ( )
109+ local_time = time ()
88110
89111 # NOTE: The server returns time in milliseconds as an int whereas
90112 # Python returns it as a float, in seconds.
91- server_time = int (struct .unpack (">Q" , response )[0 ])
92-
93- return server_time - int (t * 1000 )
113+ return server_time - int (local_time * 1000 )
94114
95115
96116def restore (serial : str , restore_code : str ) -> str :
97- restore_code = restore_code .upper ()
98117 serial = normalize_serial (serial )
118+ restore_code = restore_code .upper ()
99119 if len (restore_code ) != 10 :
100120 raise ValueError (f"invalid restore code (should be 10 characters): { restore_code } " )
101121
102- challenge = initiate_paper_restore (serial )
103- if len (challenge ) != 32 :
104- raise ValueError ("Bad challenge length (expected 32, got %i)" % (len (challenge )))
122+ client = APIClient ()
123+ challenge = client .initiate_paper_restore (serial )
105124
106125 code = restore_code_to_bytes (restore_code )
107126 hash = hmac .new (code , serial .encode () + challenge , digestmod = sha1 ).digest ()
108127
109128 otp = token_bytes (20 )
110- e = encrypt (hash + otp )
111- response = validate_paper_restore (serial + e )
129+ encrypted_data = encrypt (hash + otp )
130+ response = client . validate_paper_restore (serial , encrypted_data )
112131 secret = decrypt (response , otp )
113132
114133 return b32encode (secret ).decode ()
115-
116-
117- def initiate_paper_restore (
118- serial : str , host : str = ENROLL_HOSTS ["default" ], path : str = PATHS ["init_restore" ]
119- ) -> bytes :
120- return _post (host , path , data = serial )
121-
122-
123- def validate_paper_restore (
124- data : str , host : str = ENROLL_HOSTS ["default" ], path : str = PATHS ["validate_restore" ]
125- ) -> bytes :
126- try :
127- response = _post (host , path , data = data )
128- except HTTPError as e :
129- if e .response .status == 600 :
130- raise HTTPError ("Invalid serial or restore key" , e .response )
131- else :
132- raise
133- return response
0 commit comments