def _process_file(self, file, shared_keys): if file['t'] == 0 or file['t'] == 1: keys = dict( keypart.split(':', 1) for keypart in file['k'].split('/') if ':' in keypart) uid = file['u'] key = None # my objects if uid in keys: key = decrypt_key(base64_to_a32(keys[uid]), self.master_key) # shared folders elif 'su' in file and 'sk' in file and ':' in file['k']: shared_key = decrypt_key(base64_to_a32(file['sk']), self.master_key) key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key) if file['su'] not in shared_keys: shared_keys[file['su']] = {} shared_keys[file['su']][file['h']] = shared_key # shared files elif file['u'] and file['u'] in shared_keys: for hkey in shared_keys[file['u']]: shared_key = shared_keys[file['u']][hkey] if hkey in keys: key = keys[hkey] key = decrypt_key(base64_to_a32(key), shared_key) break if file['h'] and file['h'] in shared_keys.get('EXP', ()): shared_key = shared_keys['EXP'][file['h']] encrypted_key = str_to_a32( base64_url_decode(file['k'].split(':')[-1])) key = decrypt_key(encrypted_key, shared_key) file['shared_folder_key'] = shared_key if key is not None: # file if file['t'] == 0: k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) file['iv'] = key[4:6] + (0, 0) file['meta_mac'] = key[6:8] # folder else: k = key file['key'] = key file['k'] = k attributes = base64_url_decode(file['a']) attributes = decrypt_attr(attributes, k) file['a'] = attributes # other => wrong object elif file['k'] == '': file['a'] = False elif file['t'] == 2: self.root_id = file['h'] file['a'] = {'n': 'Cloud Drive'} elif file['t'] == 3: self.inbox_id = file['h'] file['a'] = {'n': 'Inbox'} elif file['t'] == 4: self.trashbin_id = file['h'] file['a'] = {'n': 'Rubbish Bin'} return file
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) self.rsa_private_key = [0, 0, 0, 0] for i in range(4): l = int( ((private_key[0]) * 256 + (private_key[1]) + 7) / 8) + 2 self.rsa_private_key[i] = mpi_to_int(private_key[:l]) private_key = private_key[l:] encrypted_sid = mpi_to_int(base64_url_decode(resp['csid'])) rsa_decrypter = RSA.construct( (self.rsa_private_key[0] * self.rsa_private_key[1], 257, self.rsa_private_key[2], self.rsa_private_key[0], self.rsa_private_key[1])) 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 get_upload_link(self, file): """ Get a files public link inc. decrypted key Requires upload() response as input """ if 'f' in file: file = file['f'][0] public_handle = self._api_request({'a': 'l', 'n': file['h']}) file_key = file['k'][file['k'].index(':') + 1:] decrypted_key = a32_to_base64( decrypt_key(base64_to_a32(file_key), self.master_key)) return (f'{self.schema}://{self.domain}' f'/#!{public_handle}!{decrypted_key}') else: raise ValueError('''Upload() response required as input, use get_link() for regular file input''')
def _init_shared_keys(self, files, shared_keys): """ Init shared key not associated with a user. Seems to happen when a folder is shared, some files are exchanged and then the folder is un-shared. Keys are stored in files['s'] and files['ok'] """ ok_dict = {} for ok_item in files['ok']: shared_key = decrypt_key(base64_to_a32(ok_item['k']), self.master_key) ok_dict[ok_item['h']] = shared_key for s_item in files['s']: if s_item['u'] not in shared_keys: shared_keys[s_item['u']] = {} if s_item['h'] in ok_dict: shared_keys[s_item['u']][s_item['h']] = ok_dict[s_item['h']] self.shared_keys = shared_keys
def get_public_file_info(self, file_handle, file_key): """ Get size and name of a public file """ data = self._api_request({'a': 'g', 'p': file_handle, 'ssm': 1}) if isinstance(data, int): raise RequestError(data) if 'at' not in data or 's' not in data: raise ValueError("Unexpected result", data) key = base64_to_a32(file_key) k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) size = data['s'] unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k) if not unencrypted_attrs: return None result = {'size': size, 'name': unencrypted_attrs['n']} return result
def import_public_file(self, file_handle, file_key, dest_node=None, dest_name=None): """ Import the public file into user account """ # Providing dest_node spare an API call to retrieve it. if dest_node is None: # Get '/Cloud Drive' folder no dest node specified dest_node = self.get_node_by_type(2)[1] # Providing dest_name spares an API call to retrieve it. if dest_name is None: pl_info = self.get_public_file_info(file_handle, file_key) dest_name = pl_info['name'] key = base64_to_a32(file_key) k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) encrypted_key = a32_to_base64(encrypt_key(key, self.master_key)) encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k)) return self._api_request({ 'a': 'p', 't': dest_node['h'], 'n': [{ 'ph': file_handle, 't': 0, 'a': encrypted_name, 'k': encrypted_key }] })
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 _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.move(temp_output_file.name, output_path) return output_path