From 936c392d580af179203c909ca89b0d65e862bd78 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Wed, 22 Sep 2021 15:48:21 -0700 Subject: [PATCH] Add account registration. --- src/mega/crypto.py | 13 +++++ src/mega/errors.py | 6 +++ src/mega/mega.py | 120 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 119 insertions(+), 20 deletions(-) diff --git a/src/mega/crypto.py b/src/mega/crypto.py index b97e102..f03f9cd 100644 --- a/src/mega/crypto.py +++ b/src/mega/crypto.py @@ -1,6 +1,7 @@ import base64 import binascii import json +import math import random import struct import sys @@ -109,8 +110,17 @@ def mpi_to_int(s): order. The first two bytes are a header which tell the number of bits in the integer. The rest of the bytes are the integer. ''' + if s == bytes([0, 0]): + return 0 return int(binascii.hexlify(s[2:]), 16) +def int_to_mpi(i): + byte_length = math.ceil(math.log(i, 256)) + bit_length = byte_length * 8 + header = bit_length.to_bytes(2, 'big') + body = i.to_bytes(byte_length, 'big') + return header + body + def extended_gcd(a, b): if a == 0: return (b, 0, 1) @@ -166,3 +176,6 @@ def make_id(length): possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" text = ''.join(random.choice(possible) for i in range(length)) return text + +def random_a32(length): + return [random.randint(0, 0xFFFFFFFF) for x in range(length)] diff --git a/src/mega/errors.py b/src/mega/errors.py index 2ad320e..80e65bc 100644 --- a/src/mega/errors.py +++ b/src/mega/errors.py @@ -20,6 +20,12 @@ class RequestError(MegaError): def __str__(self): return self.message +class RegistrationError(MegaError): + pass + +class RegistrationChallengeFailed(RegistrationError): + pass + class EINTERNAL(RequestError): code = -1 message = ( diff --git a/src/mega/mega.py b/src/mega/mega.py index d6a86e5..9482c75 100644 --- a/src/mega/mega.py +++ b/src/mega/mega.py @@ -36,9 +36,9 @@ class Mega: def __init__(self): self.schema = 'https' self.domain = 'mega.co.nz' - self.timeout = 160 # max secs to wait for resp from api requests + self.timeout = 160 self.sid = None - self.sequence_num = random.randint(0, 0xFFFFFFFF) + self.sequence_num = crypto.random_a32(length=1)[0] self.request_id = crypto.make_id(10) self._cached_trash_folder_node_id = None self._cached_root_node_id = None @@ -49,6 +49,7 @@ class Mega: retry=tenacity.retry_if_exception_type((errors.EAGAIN, json.decoder.JSONDecodeError)), stop=tenacity.stop_after_attempt(10), wait=tenacity.wait_exponential(multiplier=2, min=2, max=60), + reraise=True, ) def _api_request(self, request_data, params={}): request_params = {'id': self.sequence_num} @@ -64,6 +65,9 @@ class Mega: request_data = [request_data] request_json = [d.request if isinstance(d, RequestDraft) else d for d in request_data] + + logger.debug('API request: %s', request_json) + response = self.requests_session.post( url=f'{self.schema}://g.api.{self.domain}/cs', params=request_params, @@ -72,6 +76,8 @@ class Mega: ) responses = json.loads(response.text) + logger.debug('API response: %s', response.text[:250]) + if isinstance(responses, int): # If this raises EAGAIN it'll be caught by tenacity retry. raise errors.error_for_code(responses) @@ -126,7 +132,7 @@ class Mega: self._cached_trash_folder_node_id = self.get_node_by_type(NODE_TYPE_TRASH)[0] return self._cached_trash_folder_node_id - # LOGIN ######################################################################################## + # LOGIN & REGISTER ############################################################################# def _api_account_version_and_salt(self, email): ''' @@ -184,21 +190,29 @@ class Mega: iterations=100000, dklen=32 ) - password_aes = crypto.str_to_a32(pbkdf2_key[:16]) + password_key = crypto.str_to_a32(pbkdf2_key[:16]) user_hash = crypto.base64_url_encode(pbkdf2_key[-16:]) else: password_a32 = crypto.str_to_a32(password) - password_aes = crypto.prepare_key(password_a32) - user_hash = crypto.stringhash(email, password_aes) + password_key = crypto.prepare_key(password_a32) + user_hash = crypto.stringhash(email, password_key) resp = self._api_start_session(email, user_hash) - self._login_process(resp, password_aes) + self._login_process(resp, password_key) - def login_anonymous(self): + def login_anonymous(self, password=None): logger.info('Logging in anonymous temporary user...') - master_key = [random.randint(0, 0xFFFFFFFF)] * 4 - password_key = [random.randint(0, 0xFFFFFFFF)] * 4 - session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 + master_key = crypto.random_a32(length=4) + + # During the registration process, we start an anonymous session that + # will become our account. This is why we can choose a password here. + if password is None: + password_key = crypto.random_a32(length=4) + else: + password_a32 = crypto.str_to_a32(password) + password_key = crypto.prepare_key(password_a32) + + session_self_challenge = crypto.random_a32(length=4) k = crypto.a32_to_base64(crypto.encrypt_key(master_key, password_key)) ts = crypto.a32_to_str(session_self_challenge) @@ -209,9 +223,75 @@ class Mega: resp = self._api_start_session(user) self._login_process(resp, password_key) - def _login_process(self, resp, password): + def register(self, email, password, name=''): + self.login_anonymous(password=password) + self._api_request({'a': 'up', 'name': name}) + + # Request signup link + challenge = tuple(crypto.random_a32(length=4)) + cdata = self.master_key + challenge + request = { + 'a': 'uc', + 'c': crypto.a32_to_base64(cdata), + 'n': crypto.base64_url_encode(name.encode('utf-8')), + 'm': crypto.base64_url_encode(email.encode('utf-8')), + } + self._api_request(request) + self._registration_challenge = challenge + + def verify_registration(self, confirmation): + if not hasattr(self, '_registration_challenge'): + message = 'You cannot call verify_registration before calling register.' + raise errors.RegistrationError(message) + + confirmation = confirmation.split('/#confirm', 1)[-1] + + request = { + 'a': 'ud', + 'c': confirmation, + } + response = self._api_request(request) + (email, name, user_id, encrypted_key, challenge) = response + + email = crypto.base64_url_decode(email).decode('utf-8').lower() + challenge = crypto.base64_to_a32(challenge) + + if challenge != self._registration_challenge: + message = f'local: {self._registration_challenge}, remote: {challenge}.' + raise errors.RegistrationChallengeFailed(message) + + user_hash = crypto.stringhash(email, self._password_key) + + self._api_request({'a': 'up', 'uh': user_hash, 'c': confirmation}) + response = self._api_start_session(email, user_hash) + self._login_process(response, self._password_key) + + private = RSA.generate(2048) + public = private.publickey() + + pubk = crypto.base64_url_encode(crypto.int_to_mpi(public.n) + crypto.int_to_mpi(public.e)) + privk = b''.join([ + crypto.int_to_mpi(private.p), + crypto.int_to_mpi(private.q), + crypto.int_to_mpi(private.d), + crypto.int_to_mpi(private.u), + ]) + padding = (len(privk) % 16) % 16 + privk += b'\x00' * padding + privk = crypto.str_to_a32(privk) + privk = crypto.encrypt_key(privk, self.master_key) + privk = crypto.a32_to_base64(privk) + request = { + 'a': 'up', + 'pubk': pubk, + 'privk': privk, + } + self._api_request(request) + + def _login_process(self, resp, password_key): encrypted_master_key = crypto.base64_to_a32(resp['k']) - self.master_key = crypto.decrypt_key(encrypted_master_key, password) + self._password_key = password_key + self.master_key = crypto.decrypt_key(encrypted_master_key, password_key) # tsid is for temporary sessions if 'tsid' in resp: tsid = crypto.base64_url_decode(resp['tsid']) @@ -233,12 +313,12 @@ class Mega: for i in range(4): # An MPI integer has a 2-byte header which describes the number # of bits in the integer. - bitlength = (private_key[0] * 256) + private_key[1] - bytelength = math.ceil(bitlength / 8) + bit_length = (private_key[0] * 256) + private_key[1] + byte_length = math.ceil(bit_length / 8) # Add 2 bytes to accommodate the MPI header - bytelength += 2 - rsa_private_key[i] = crypto.mpi_to_int(private_key[:bytelength]) - private_key = private_key[bytelength:] + byte_length += 2 + rsa_private_key[i] = crypto.mpi_to_int(private_key[:byte_length]) + private_key = private_key[byte_length:] first_factor_p = rsa_private_key[0] second_factor_q = rsa_private_key[1] @@ -367,7 +447,7 @@ class Mega: def _mkdir(self, name, parent_node_id): # generate random aes key (128) for folder - ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] + ul_key = crypto.random_a32(length=6) # encrypt attribs attribs = {'n': name} @@ -1371,7 +1451,7 @@ class Mega: ul_url = self._api_request({'a': 'u', 's': file_size})['p'] # generate random aes key (128) for file - ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] + ul_key = crypto.random_a32(length=6) k_str = crypto.a32_to_str(ul_key[:4]) count = Counter.new( 128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64)