def login_ephemeral(self): random_master_key = [random.randint(0, 0xFFFFFFFF)] * 4 random_password_key = [random.randint(0, 0xFFFFFFFF)] * 4 random_session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 user_handle = self.api_req({ 'a': 'up', 'k': a32_to_base64(encrypt_key(random_master_key, random_password_key)), 'ts': base64urlencode(a32_to_str(random_session_self_challenge) + a32_to_str(encrypt_key( random_session_self_challenge, random_master_key))) }) res = self.api_req({'a': 'us', 'user': user_handle}) self._login_common(res, random_password_key)
def _mkdir(self, name, parent_node_id): # generate random aes key (128) for folder ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] # encrypt attribs attribs = {'n': name} encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key)) # update attributes data = self._api_request({ 'a': 'p', 't': parent_node_id, 'n': [{ 'h': 'xxxxxxxx', 't': 1, 'a': encrypt_attribs, 'k': encrypted_key }], 'i': self.request_id }) return data
def _login_common(self, res, password): if res in (-2, -9): raise MegaIncorrectPasswordExcetion("Incorrect e-mail and/or password.") enc_master_key = base64_to_a32(res['k']) self.master_key = decrypt_key(enc_master_key, password) if 'tsid' in res: tsid = base64urldecode(res['tsid']) key_encrypted = a32_to_str( encrypt_key(str_to_a32(tsid[:16]), self.master_key)) if key_encrypted == tsid[-16:]: self.sid = res['tsid'] elif 'csid' in res: enc_rsa_priv_key = base64_to_a32(res['privk']) rsa_priv_key = decrypt_key(enc_rsa_priv_key, self.master_key) privk = a32_to_str(rsa_priv_key) self.rsa_priv_key = [0, 0, 0, 0] for i in range(4): l = ((ord(privk[0]) * 256 + ord(privk[1]) + 7) / 8) + 2 self.rsa_priv_key[i] = mpi2int(privk[:l]) privk = privk[l:] enc_sid = mpi2int(base64urldecode(res['csid'])) decrypter = RSA.construct( (self.rsa_priv_key[0] * self.rsa_priv_key[1], 0, self.rsa_priv_key[2], self.rsa_priv_key[0], self.rsa_priv_key[1])) sid = '%x' % decrypter.key._decrypt(enc_sid) sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid) self.sid = base64urlencode(sid[:43])
def make_wallet(root_key, date, passphrase, fake_passphrase, kdf_type, salt): """ make_wallet(root_key, date, passphrase, fake_passphrase, kdf_type, salt) root_key :: 32 bytes (pseudo)-random data. date :: Wallet creation date. Integer <= 65535. == ((creation day) - (2013-01-01)) / 7 passphrase :: The passphrase to decrypt the wallet. fake_passphrase :: Alternate passphrase. Decrypts to a different wallet. kdf_type :: The numerical code for the desired KDF type. salt :: A 2-byte random value. Returns a wallet ciphertext, which can be decrypted with the passphrase. Format: [12 bit salt][4 bit KDF type][2 byte creation date][4 byte checksum][32 byte encrypted root_key] Salt goes first because SSSS works better with random most significant bits. """ assert(len(root_key) == 32) assert(date >= 0 and date <= 65535) assert(kdf_type in (0,1,2,8,9)) assert(len(salt) == 2) big_endian_date = chr(date >> 8) + chr(date & 0xff) # Encode the KDF in the bottom 4 bits of the salt kdf_byte = chr( kdf_type + (ord(salt[1]) & 0xF0) ) salt = salt[0] + kdf_byte # Use the provided salt value + the date as the salt new_salt = salt + big_endian_date # 4 byte salt kdf = crypto.kdfs[kdf_type] encrypted_root_key = crypto.encrypt_key(root_key, new_salt, passphrase, kdf) fake_root_key = crypto.decrypt_key(encrypted_root_key, new_salt, fake_passphrase, kdf) decryption_checksum = crypto.make_key_checksum(root_key, fake_root_key) return salt + big_endian_date + decryption_checksum + encrypted_root_key
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 _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 rename(self, file, new_name): file = file[1] # create new attribs attribs = {'n': new_name} # encrypt attribs encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k'])) encrypted_key = a32_to_base64(encrypt_key(file['key'], self.master_key)) # update attributes return self._api_request([{ 'a': 'a', 'attr': encrypt_attribs, 'key': encrypted_key, 'n': file['h'], 'i': self.request_id }])
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 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 uploadfile(self, filename, dst=None): if not dst: root_id = getattr(self, 'root_id', None) if root_id == None: self.get_files() dst = self.root_id infile = open(filename, 'rb') size = os.path.getsize(filename) ul_url = self.api_req({'a': 'u', 's': size})['p'] ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] counter = Counter.new( 128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64) encryptor = AES.new( a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=counter) file_mac = [0, 0, 0, 0] for chunk_start, chunk_size in sorted(get_chunks(size).items()): chunk = infile.read(chunk_size) chunk_mac = [ul_key[4], ul_key[5], ul_key[4], ul_key[5]] for i in range(0, len(chunk), 16): block = chunk[i:i+16] if len(block) % 16: block += '\0' * (16 - len(block) % 16) block = str_to_a32(block) chunk_mac = [chunk_mac[0] ^ block[0], chunk_mac[1] ^ block[1], chunk_mac[2] ^ block[2], chunk_mac[3] ^ block[3]] chunk_mac = aes_cbc_encrypt_a32(chunk_mac, ul_key[:4]) file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1], file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]] file_mac = aes_cbc_encrypt_a32(file_mac, ul_key[:4]) chunk = encryptor.encrypt(chunk) url = '%s/%s' % (ul_url, str(chunk_start)) outfile = requests.post(url, data=chunk, stream=True).raw completion_handle = outfile.read() infile.close() meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) attributes = {'n': os.path.basename(filename)} enc_attributes = base64urlencode(enc_attr(attributes, 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)) data = self.api_req({'a': 'p', 't': dst, 'n': [ {'h': completion_handle, 't': 0, 'a': enc_attributes, 'k': encrypted_key}]}) return data