def export(self, path=None, node_id=None): nodes = self.get_files() if node_id: node = nodes[node_id] else: node = self.find(path) node_data = self._node_data(node) is_file_node = node_data['t'] == 0 if is_file_node: return self._export_file(node) if node: try: # If already exported return self.get_folder_link(node) except (RequestError, KeyError): pass master_key_cipher = AES.new(a32_to_str(self.master_key), AES.MODE_ECB) ha = base64_url_encode( master_key_cipher.encrypt(node_data['h'].encode("utf8") + node_data['h'].encode("utf8"))) share_key = secrets.token_bytes(16) ok = base64_url_encode(master_key_cipher.encrypt(share_key)) share_key_cipher = AES.new(share_key, AES.MODE_ECB) node_key = node_data['k'] encrypted_node_key = base64_url_encode( share_key_cipher.encrypt(a32_to_str(node_key))) node_id = node_data['h'] request_body = [{ 'a': 's2', 'n': node_id, 's': [{ 'u': 'EXP', 'r': 0 }], 'i': self.request_id, 'ok': ok, 'ha': ha, 'cr': [[node_id], [node_id], [0, 0, encrypted_node_key]] }] self._api_request(request_body) nodes = self.get_files() return self.get_folder_link(nodes[node_id])
def _login_process(self, resp, password): encrypted_master_key = base64_to_a32(resp['k']) self.master_key = decrypt_key(encrypted_master_key, password) if 'tsid' in resp: tsid = base64_url_decode(resp['tsid']) key_encrypted = a32_to_str( encrypt_key(str_to_a32(tsid[:16]), self.master_key)) if key_encrypted == tsid[-16:]: self.sid = resp['tsid'] elif 'csid' in resp: encrypted_rsa_private_key = base64_to_a32(resp['privk']) rsa_private_key = decrypt_key(encrypted_rsa_private_key, self.master_key) private_key = a32_to_str(rsa_private_key) # The private_key contains 4 MPI integers concatenated together. rsa_private_key = [0, 0, 0, 0] 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) # Add 2 bytes to accommodate the MPI header bytelength += 2 rsa_private_key[i] = mpi_to_int(private_key[:bytelength]) private_key = private_key[bytelength:] first_factor_p = rsa_private_key[0] second_factor_q = rsa_private_key[1] private_exponent_d = rsa_private_key[2] # In MEGA's webclient javascript, they assign [3] to a variable # called u, but I do not see how it corresponds to pycryptodome's # RSA.construct and it does not seem to be necessary. rsa_modulus_n = first_factor_p * second_factor_q phi = (first_factor_p - 1) * (second_factor_q - 1) public_exponent_e = modular_inverse(private_exponent_d, phi) rsa_components = ( rsa_modulus_n, public_exponent_e, private_exponent_d, first_factor_p, second_factor_q, ) rsa_decrypter = RSA.construct(rsa_components) encrypted_sid = mpi_to_int(base64_url_decode(resp['csid'])) sid = '%x' % rsa_decrypter._decrypt(encrypted_sid) sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid) self.sid = base64_url_encode(sid[:43])
def decrypt(self): url_parts = self.link.split("!") file_id, file_key = url_parts[1], url_parts[2] key = crypto.base64_to_a32(file_key) k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) iv = key[4:6] + (0, 0) file_path = os.path.join(self.path, self.name) size = os.path.getsize(file_path) out_file_path = os.path.join(self.path, self.out_name) decryptor = AES.new(crypto.a32_to_str(k), AES.MODE_CTR, counter=Counter.new( 128, initial_value=((iv[0] << 32) + iv[1]) << 64)) with open(file_path, 'rb') as fh_in: with open(out_file_path, 'wb') as fh_out: for chunk_start, chunk_size in sorted( crypto.get_chunks(size).items()): chunk = fh_in.read(chunk_size) chunk = decryptor.decrypt(chunk) fh_out.write(chunk)
def login_anonymous(self): 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 user = self._api_request({ 'a': 'up', 'k': a32_to_base64(encrypt_key(master_key, password_key)), 'ts': base64_url_encode( a32_to_str(session_self_challenge) + a32_to_str(encrypt_key(session_self_challenge, master_key))) }) resp = self._api_request({'a': 'us', 'user': user}) if isinstance(resp, int): raise RequestError(resp) self._login_process(resp, password_key)
def decrypt(self): url_parts = self.link.split("!") file_id, file_key = url_parts[1], url_parts[2] key = crypto.base64_to_a32(file_key) k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) iv = key[4:6] + (0, 0) file_path = os.path.join(self.path, self.name) size = os.path.getsize(file_path) out_file_path = os.path.join(self.path, self.out_name) decryptor = AES.new(crypto.a32_to_str(k), AES.MODE_CTR, counter=Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64)) with open(file_path, 'rb') as fh_in: with open(out_file_path, 'wb') as fh_out: for chunk_start, chunk_size in sorted(crypto.get_chunks(size).items()): chunk = fh_in.read(chunk_size) chunk = decryptor.decrypt(chunk) fh_out.write(chunk) # mac checking omitted
def _login_user(self, email, password): logger.info('Logging in user...') email = email.lower() get_user_salt_resp = self._api_request({'a': 'us0', 'user': email}) user_salt = None try: user_salt = base64_to_a32(get_user_salt_resp['s']) except KeyError: # v1 user account password_aes = prepare_key(str_to_a32(password)) user_hash = stringhash(email, password_aes) else: # v2 user account pbkdf2_key = hashlib.pbkdf2_hmac(hash_name='sha512', password=password.encode(), salt=a32_to_str(user_salt), iterations=100000, dklen=32) password_aes = str_to_a32(pbkdf2_key[:16]) user_hash = base64_url_encode(pbkdf2_key[-16:]) resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash}) if isinstance(resp, int): raise RequestError(resp) self._login_process(resp, password_aes)
def upload(self, filename, dest=None, dest_filename=None): # determine storage node if dest is None: # if none set, upload to cloud drive node if not hasattr(self, 'root_id'): self.get_files() dest = self.root_id # request upload url, call 'u' method with open(filename, 'rb') as input_file: file_size = os.path.getsize(filename) 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)] k_str = a32_to_str(ul_key[:4]) count = Counter.new( 128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64) aes = AES.new(k_str, AES.MODE_CTR, counter=count) upload_progress = 0 completion_file_handle = None mac_str = '\0' * 16 mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str.encode("utf8")) iv_str = a32_to_str([ul_key[4], ul_key[5], ul_key[4], ul_key[5]]) if file_size > 0: for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) upload_progress += len(chunk) encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) for i in range(0, len(chunk) - 16, 16): block = chunk[i:i + 16] encryptor.encrypt(block) # fix for files under 16 bytes failing if file_size > 16: i += 16 else: i = 0 block = chunk[i:i + 16] if len(block) % 16: block += makebyte('\0' * (16 - len(block) % 16)) mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) # encrypt file and upload chunk = aes.encrypt(chunk) output_file = requests.post(ul_url + "/" + str(chunk_start), data=chunk, timeout=self.timeout) completion_file_handle = output_file.text logger.info('%s of %s uploaded', upload_progress, file_size) else: output_file = requests.post(ul_url + "/0", data='', timeout=self.timeout) completion_file_handle = output_file.text logger.info('Chunks uploaded') logger.info('Setting attributes to complete upload') logger.info('Computing attributes') file_mac = str_to_a32(mac_str) # determine meta mac meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) dest_filename = dest_filename or os.path.basename(filename) attribs = {'n': dest_filename} encrypt_attribs = base64_url_encode( encrypt_attr(attribs, ul_key[:4])) key = [ ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1] ] encrypted_key = a32_to_base64(encrypt_key(key, self.master_key)) logger.info('Sending request to update attributes') # update attributes data = self._api_request({ 'a': 'p', 't': dest, 'i': self.request_id, 'n': [{ 'h': completion_file_handle, 't': 0, 'a': encrypt_attribs, 'k': encrypted_key }] }) logger.info('Upload complete') return data
def _download_file(self, file_handle, file_key, dest_path=None, dest_filename=None, is_public=False, file=None): if file is None: if is_public: file_key = base64_to_a32(file_key) file_data = self._api_request({ 'a': 'g', 'g': 1, 'p': file_handle }) else: file_data = self._api_request({ 'a': 'g', 'g': 1, 'n': file_handle }) k = (file_key[0] ^ file_key[4], file_key[1] ^ file_key[5], file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) iv = file_key[4:6] + (0, 0) meta_mac = file_key[6:8] else: file_data = self._api_request({'a': 'g', 'g': 1, 'n': file['h']}) k = file['k'] iv = file['iv'] meta_mac = file['meta_mac'] # Seems to happens sometime... When this occurs, files are # inaccessible also in the official also in the official web app. # Strangely, files can come back later. if 'g' not in file_data: raise RequestError('File not accessible anymore') file_url = file_data['g'] file_size = file_data['s'] attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) if dest_filename is not None: file_name = dest_filename else: file_name = attribs['n'] input_file = requests.get(file_url, stream=True).raw if dest_path is None: dest_path = '' else: dest_path += '/' with tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False) as temp_output_file: k_str = a32_to_str(k) counter = Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64) aes = AES.new(k_str, AES.MODE_CTR, counter=counter) mac_str = '\0' * 16 mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str.encode("utf8")) iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]]) for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) chunk = aes.decrypt(chunk) temp_output_file.write(chunk) encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) for i in range(0, len(chunk) - 16, 16): block = chunk[i:i + 16] encryptor.encrypt(block) # fix for files under 16 bytes failing if file_size > 16: i += 16 else: i = 0 block = chunk[i:i + 16] if len(block) % 16: block += b'\0' * (16 - (len(block) % 16)) mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) file_info = os.stat(temp_output_file.name) logger.info('%s of %s downloaded', file_info.st_size, file_size) file_mac = str_to_a32(mac_str) # check mac integrity if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') output_path = Path(dest_path + file_name) shutil.copy(temp_output_file.name, output_path) return output_path