various fixes

This commit is contained in:
Richard O'Dwyer 2019-10-16 21:20:22 +01:00
parent 52471551a8
commit 00c2fb2192
12 changed files with 390 additions and 203 deletions

3
.gitignore vendored
View file

@ -9,4 +9,5 @@
build/ build/
dist/ dist/
.pypirc .pypirc
MANIFEST MANIFEST
.vscode/

View file

@ -1,5 +1,5 @@
language: python language: python
python: python:
- "2.7" - "2.7"
install: pip install -r requirements.txt install: pip install -r requirements-dev.txt
script: python tests/unit-tests.py script: python setup.py install && pytest tests/unit-tests.py -x -s -v

View file

@ -1,3 +1,4 @@
import os
from mega import Mega from mega import Mega
@ -8,12 +9,12 @@ def test():
see readme.md for more information see readme.md for more information
""" """
#user details # user details
email = 'your@email.com' email = os.environ['EMAIL']
password = 'password' password = os.environ['PASS']
mega = Mega() mega = Mega()
#mega = Mega({'verbose': True}) # verbose option for print output # mega = Mega({'verbose': True}) # verbose option for print output
# login # login
m = mega.login(email, password) m = mega.login(email, password)
@ -47,16 +48,17 @@ def test():
# download file. by file object or url # download file. by file object or url
print m.download(file, '/tmp') print m.download(file, '/tmp')
#m.download_url(link) # m.download_url(link)
#delete or destroy file. by id or url # delete or destroy file. by id or url
print(m.delete(file[0])) print(m.delete(file[0]))
#print(m.destroy(file[0])) # print(m.destroy(file[0]))
#print(m.delete_url(link)) # print(m.delete_url(link))
#print(m.destroy_url(link)) # print(m.destroy_url(link))
# empty trash # empty trash
print(m.empty_trash()) print(m.empty_trash())
if __name__ == '__main__': if __name__ == '__main__':
test() test()

View file

@ -1 +1 @@
from .mega import Mega from .mega import Mega # noqa

View file

@ -48,14 +48,14 @@ def prepare_key(arr):
def encrypt_key(a, key): def encrypt_key(a, key):
return sum( return sum(
(aes_cbc_encrypt_a32(a[i:i + 4], key) (aes_cbc_encrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
for i in range(0, len(a), 4)), ()) )
def decrypt_key(a, key): def decrypt_key(a, key):
return sum( return sum(
(aes_cbc_decrypt_a32(a[i:i + 4], key) (aes_cbc_decrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
for i in range(0, len(a), 4)), ()) )
def encrypt_attr(attr, key): def encrypt_attr(attr, key):
@ -86,7 +86,7 @@ def mpi_to_int(s):
def base64_url_decode(data): def base64_url_decode(data):
data += '=='[(2 - len(data) * 3) % 4:] data += '==' [(2 - len(data) * 3) % 4:]
for search, replace in (('-', '+'), ('_', '/'), (',', '')): for search, replace in (('-', '+'), ('_', '/'), (',', '')):
data = data.replace(search, replace) data = data.replace(search, replace)
return base64.b64decode(data) return base64.b64decode(data)
@ -110,12 +110,12 @@ def a32_to_base64(a):
def get_chunks(size): def get_chunks(size):
p = 0 p = 0
s = 0x20000 s = 0x20000
while p+s < size: while p + s < size:
yield(p, s) yield (p, s)
p += s p += s
if s < 0x100000: if s < 0x100000:
s += 0x20000 s += 0x20000
yield(p, size-p) yield (p, size - p)
# more general functions # more general functions

View file

@ -9,6 +9,5 @@ class RequestError(Exception):
""" """
Error in API request Error in API request
""" """
#TODO add error response messages # TODO add error response messages
pass pass

View file

@ -9,7 +9,11 @@ import binascii
import requests import requests
import shutil import shutil
from .errors import ValidationError, RequestError from .errors import ValidationError, RequestError
from .crypto import * from .crypto import (
a32_to_base64, encrypt_key, base64_url_encode, encrypt_attr, base64_to_a32,
base64_url_decode, decrypt_attr, a32_to_str, get_chunks, str_to_a32,
decrypt_key, mpi_to_int, stringhash, prepare_key, make_id,
)
import tempfile import tempfile
@ -17,7 +21,7 @@ class Mega(object):
def __init__(self, options=None): def __init__(self, options=None):
self.schema = 'https' self.schema = 'https'
self.domain = 'mega.co.nz' self.domain = 'mega.co.nz'
self.timeout = 160 # max time (secs) to wait for resp from api requests self.timeout = 160 # max secs to wait for resp from api requests
self.sid = None self.sid = None
self.sequence_num = random.randint(0, 0xFFFFFFFF) self.sequence_num = random.randint(0, 0xFFFFFFFF)
self.request_id = make_id(10) self.request_id = make_id(10)
@ -37,7 +41,7 @@ class Mega(object):
password_aes = prepare_key(str_to_a32(password)) password_aes = prepare_key(str_to_a32(password))
uh = stringhash(email, password_aes) uh = stringhash(email, password_aes)
resp = self._api_request({'a': 'us', 'user': email, 'uh': uh}) resp = self._api_request({'a': 'us', 'user': email, 'uh': uh})
#if numeric error code response # if numeric error code response
if isinstance(resp, int): if isinstance(resp, int):
raise RequestError(resp) raise RequestError(resp)
self._login_process(resp, password_aes) self._login_process(resp, password_aes)
@ -47,15 +51,23 @@ class Mega(object):
password_key = [random.randint(0, 0xFFFFFFFF)] * 4 password_key = [random.randint(0, 0xFFFFFFFF)] * 4
session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4
user = self._api_request({ user = self._api_request(
'a': 'up', {
'k': a32_to_base64(encrypt_key(master_key, password_key)), 'a':
'ts': base64_url_encode(a32_to_str(session_self_challenge) + 'up',
a32_to_str(encrypt_key(session_self_challenge, master_key))) '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}) resp = self._api_request({'a': 'us', 'user': user})
#if numeric error code response # if numeric error code response
if isinstance(resp, int): if isinstance(resp, int):
raise RequestError(resp) raise RequestError(resp)
self._login_process(resp, password_key) self._login_process(resp, password_key)
@ -66,27 +78,34 @@ class Mega(object):
if 'tsid' in resp: if 'tsid' in resp:
tsid = base64_url_decode(resp['tsid']) tsid = base64_url_decode(resp['tsid'])
key_encrypted = a32_to_str( key_encrypted = a32_to_str(
encrypt_key(str_to_a32(tsid[:16]), self.master_key)) encrypt_key(str_to_a32(tsid[:16]), self.master_key)
)
if key_encrypted == tsid[-16:]: if key_encrypted == tsid[-16:]:
self.sid = resp['tsid'] self.sid = resp['tsid']
elif 'csid' in resp: elif 'csid' in resp:
encrypted_rsa_private_key = base64_to_a32(resp['privk']) encrypted_rsa_private_key = base64_to_a32(resp['privk'])
rsa_private_key = decrypt_key(encrypted_rsa_private_key, rsa_private_key = decrypt_key(
self.master_key) encrypted_rsa_private_key, self.master_key
)
private_key = a32_to_str(rsa_private_key) private_key = a32_to_str(rsa_private_key)
self.rsa_private_key = [0, 0, 0, 0] self.rsa_private_key = [0, 0, 0, 0]
for i in range(4): for i in range(4):
l = ((ord(private_key[0]) * 256 + ord(private_key[1]) + 7) / 8) + 2 l = (
(ord(private_key[0]) * 256 + ord(private_key[1]) + 7) / 8
) + 2
self.rsa_private_key[i] = mpi_to_int(private_key[:l]) self.rsa_private_key[i] = mpi_to_int(private_key[:l])
private_key = private_key[l:] private_key = private_key[l:]
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid'])) encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
rsa_decrypter = RSA.construct( rsa_decrypter = RSA.construct(
(self.rsa_private_key[0] * self.rsa_private_key[1], (
0L, self.rsa_private_key[2], self.rsa_private_key[0], self.rsa_private_key[0] * self.rsa_private_key[1], 0L,
self.rsa_private_key[1])) self.rsa_private_key[2], self.rsa_private_key[0],
self.rsa_private_key[1]
)
)
sid = '%x' % rsa_decrypter.key._decrypt(encrypted_sid) sid = '%x' % rsa_decrypter.key._decrypt(encrypted_sid)
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid) sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
@ -99,24 +118,36 @@ class Mega(object):
if self.sid: if self.sid:
params.update({'sid': self.sid}) params.update({'sid': self.sid})
#ensure input data is a list # ensure input data is a list
if not isinstance(data, list): if not isinstance(data, list):
data = [data] data = [data]
url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
req = requests.post( req = requests.post(
'{0}://g.api.{1}/cs'.format(self.schema, self.domain), url,
params=params, params=params,
data=json.dumps(data), data=json.dumps(data),
timeout=self.timeout) timeout=self.timeout,
headers={
'Origin':
'https://mega.nz',
'Referer':
'https://mega.nz/login',
'User-Agent': (
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) '
'Gecko/20100101 Firefox/69.0'
),
}
)
json_resp = json.loads(req.text) json_resp = json.loads(req.text)
#if numeric error code response # if numeric error code response
if isinstance(json_resp, int): if isinstance(json_resp, int):
raise RequestError(json_resp) raise RequestError(json_resp)
return json_resp[0] return json_resp[0]
def _parse_url(self, url): def _parse_url(self, url):
#parse file id and key from url # parse file id and key from url
if '!' in url: if '!' in url:
match = re.findall(r'/#!(.*)', url) match = re.findall(r'/#!(.*)', url)
path = match[0] path = match[0]
@ -129,15 +160,21 @@ class Mega(object):
Process a file Process a file
""" """
if file['t'] == 0 or file['t'] == 1: if file['t'] == 0 or file['t'] == 1:
keys = dict(keypart.split(':', 1) for keypart in file['k'].split('/') if ':' in keypart) keys = dict(
keypart.split(':', 1)
for keypart in file['k'].split('/')
if ':' in keypart
)
uid = file['u'] uid = file['u']
key = None key = None
# my objects # my objects
if uid in keys: if uid in keys:
key = decrypt_key(base64_to_a32(keys[uid]), self.master_key) key = decrypt_key(base64_to_a32(keys[uid]), self.master_key)
# shared folders # shared folders
elif 'su' in file and 'sk' in file and ':' in file['k']: elif 'su' in file and 'sk' in file and ':' in file['k']:
shared_key = decrypt_key(base64_to_a32(file['sk']), self.master_key) shared_key = decrypt_key(
base64_to_a32(file['sk']), self.master_key
)
key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key) key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key)
if file['su'] not in shared_keys: if file['su'] not in shared_keys:
shared_keys[file['su']] = {} shared_keys[file['su']] = {}
@ -153,8 +190,10 @@ class Mega(object):
if key is not None: if key is not None:
# file # file
if file['t'] == 0: if file['t'] == 0:
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], k = (
key[3] ^ key[7]) key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
key[3] ^ key[7]
)
file['iv'] = key[4:6] + (0, 0) file['iv'] = key[4:6] + (0, 0)
file['meta_mac'] = key[6:8] file['meta_mac'] = key[6:8]
# folder # folder
@ -189,7 +228,9 @@ class Mega(object):
""" """
ok_dict = {} ok_dict = {}
for ok_item in files['ok']: for ok_item in files['ok']:
shared_key = decrypt_key(base64_to_a32(ok_item['k']), self.master_key) shared_key = decrypt_key(
base64_to_a32(ok_item['k']), self.master_key
)
ok_dict[ok_item['h']] = shared_key ok_dict[ok_item['h']] = shared_key
for s_item in files['s']: for s_item in files['s']:
if s_item['u'] not in shared_keys: if s_item['u'] not in shared_keys:
@ -199,7 +240,7 @@ class Mega(object):
########################################################################## ##########################################################################
# GET # GET
def find_path_descriptor(self, path): def find_path_descriptor(self, path):
""" """
Find descriptor of folder inside a path. i.e.: folder1/folder2/folder3 Find descriptor of folder inside a path. i.e.: folder1/folder2/folder3
@ -226,13 +267,15 @@ class Mega(object):
else: else:
return None return None
return parent_desc return parent_desc
def find(self, filename): def find(self, filename):
""" """
Return file object from given filename Return file object from given filename
""" """
files = self.get_files() files = self.get_files()
for file in files.items(): for file in files.items():
if not isinstance(file[1]['a'], dict):
continue
if file[1]['a'] and file[1]['a']['n'] == filename: if file[1]['a'] and file[1]['a']['n'] == filename:
return file return file
@ -246,7 +289,7 @@ class Mega(object):
self._init_shared_keys(files, shared_keys) self._init_shared_keys(files, shared_keys)
for file in files['f']: for file in files['f']:
processed_file = self._process_file(file, shared_keys) processed_file = self._process_file(file, shared_keys)
#ensure each file has a name before returning # ensure each file has a name before returning
if processed_file['a']: if processed_file['a']:
files_dict[file['h']] = processed_file files_dict[file['h']] = processed_file
return files_dict return files_dict
@ -260,15 +303,17 @@ class Mega(object):
file = file['f'][0] file = file['f'][0]
public_handle = self._api_request({'a': 'l', 'n': file['h']}) public_handle = self._api_request({'a': 'l', 'n': file['h']})
file_key = file['k'][file['k'].index(':') + 1:] file_key = file['k'][file['k'].index(':') + 1:]
decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), decrypted_key = a32_to_base64(
self.master_key)) decrypt_key(base64_to_a32(file_key), self.master_key)
return '{0}://{1}/#!{2}!{3}'.format(self.schema, )
self.domain, return '{0}://{1}/#!{2}!{3}'.format(
public_handle, self.schema, self.domain, public_handle, decrypted_key
decrypted_key) )
else: else:
raise ValueError('''Upload() response required as input, raise ValueError(
use get_link() for regular file input''') '''Upload() response required as input,
use get_link() for regular file input'''
)
def get_link(self, file): def get_link(self, file):
""" """
@ -278,12 +323,14 @@ class Mega(object):
if 'h' in file and 'k' in file: if 'h' in file and 'k' in file:
public_handle = self._api_request({'a': 'l', 'n': file['h']}) public_handle = self._api_request({'a': 'l', 'n': file['h']})
if public_handle == -11: if public_handle == -11:
raise RequestError("Can't get a public link from that file (is this a shared file?)") raise RequestError(
"Can't get a public link from that file "
"(is this a shared file?)"
)
decrypted_key = a32_to_base64(file['key']) decrypted_key = a32_to_base64(file['key'])
return '{0}://{1}/#!{2}!{3}'.format(self.schema, return '{0}://{1}/#!{2}!{3}'.format(
self.domain, self.schema, self.domain, public_handle, decrypted_key
public_handle, )
decrypted_key)
else: else:
raise ValidationError('File id and key must be present') raise ValidationError('File id and key must be present')
@ -326,7 +373,7 @@ class Mega(object):
return files_dict return files_dict
def get_id_from_public_handle(self, public_handle): def get_id_from_public_handle(self, public_handle):
#get node data # get node data
node_data = self._api_request({'a': 'f', 'f': 1, 'p': public_handle}) node_data = self._api_request({'a': 'f', 'f': 1, 'p': public_handle})
node_id = self.get_id_from_obj(node_data) node_id = self.get_id_from_obj(node_data)
return node_id return node_id
@ -338,7 +385,7 @@ class Mega(object):
node_id = None node_id = None
for i in node_data['f']: for i in node_data['f']:
if i['h'] is not u'': if i['h'] != u'':
node_id = i['h'] node_id = i['h']
return node_id return node_id
@ -346,8 +393,15 @@ class Mega(object):
""" """
Get current remaining disk quota in MegaBytes Get current remaining disk quota in MegaBytes
""" """
json_resp = self._api_request({'a': 'uq', 'xfer': 1}) json_resp = self._api_request(
#convert bytes to megabyes {
'a': 'uq',
'xfer': 1,
'strg': 1,
'v': 1
}
)
# convert bytes to megabyes
return json_resp['mstrg'] / 1048576 return json_resp['mstrg'] / 1048576
def get_storage_space(self, giga=False, mega=False, kilo=False): def get_storage_space(self, giga=False, mega=False, kilo=False):
@ -402,9 +456,13 @@ class Mega(object):
""" """
Destroy a file by its private id Destroy a file by its private id
""" """
return self._api_request({'a': 'd', return self._api_request(
'n': file_id, {
'i': self.request_id}) 'a': 'd',
'n': file_id,
'i': self.request_id
}
)
def destroy_url(self, url): def destroy_url(self, url):
""" """
@ -423,9 +481,7 @@ class Mega(object):
if files != {}: if files != {}:
post_list = [] post_list = []
for file in files: for file in files:
post_list.append({"a": "d", post_list.append({"a": "d", "n": file, "i": self.request_id})
"n": file,
"i": self.request_id})
return self._api_request(post_list) return self._api_request(post_list)
########################################################################## ##########################################################################
@ -434,7 +490,14 @@ class Mega(object):
""" """
Download a file by it's file object Download a file by it's file object
""" """
self._download_file(None, None, file=file[1], dest_path=dest_path, dest_filename=dest_filename, is_public=False) self._download_file(
None,
None,
file=file[1],
dest_path=dest_path,
dest_filename=dest_filename,
is_public=False
)
def download_url(self, url, dest_path=None, dest_filename=None): def download_url(self, url, dest_path=None, dest_filename=None):
""" """
@ -443,18 +506,42 @@ class Mega(object):
path = self._parse_url(url).split('!') path = self._parse_url(url).split('!')
file_id = path[0] file_id = path[0]
file_key = path[1] file_key = path[1]
self._download_file(file_id, file_key, dest_path, dest_filename, is_public=True) self._download_file(
file_id, file_key, dest_path, dest_filename, is_public=True
)
def _download_file(self, file_handle, file_key, dest_path=None, dest_filename=None, is_public=False, file=None): def _download_file(
self,
file_handle,
file_key,
dest_path=None,
dest_filename=None,
is_public=False,
file=None
):
if file is None: if file is None:
if is_public: if is_public:
file_key = base64_to_a32(file_key) file_key = base64_to_a32(file_key)
file_data = self._api_request({'a': 'g', 'g': 1, 'p': file_handle}) file_data = self._api_request(
{
'a': 'g',
'g': 1,
'p': file_handle
}
)
else: else:
file_data = self._api_request({'a': 'g', 'g': 1, 'n': file_handle}) 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], k = (
file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) 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) iv = file_key[4:6] + (0, 0)
meta_mac = file_key[6:8] meta_mac = file_key[6:8]
else: else:
@ -463,7 +550,7 @@ class Mega(object):
iv = file['iv'] iv = file['iv']
meta_mac = file['meta_mac'] meta_mac = file['meta_mac']
# Seems to happens sometime... When this occurs, files are # Seems to happens sometime... When this occurs, files are
# inaccessible also in the official also in the official web app. # inaccessible also in the official also in the official web app.
# Strangely, files can come back later. # Strangely, files can come back later.
if 'g' not in file_data: if 'g' not in file_data:
@ -485,11 +572,12 @@ class Mega(object):
else: else:
dest_path += '/' dest_path += '/'
temp_output_file = tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False) temp_output_file = tempfile.NamedTemporaryFile(
mode='w+b', prefix='megapy_', delete=False
)
k_str = a32_to_str(k) k_str = a32_to_str(k)
counter = Counter.new( counter = Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64)
128, initial_value=((iv[0] << 32) + iv[1]) << 64)
aes = AES.new(k_str, AES.MODE_CTR, counter=counter) aes = AES.new(k_str, AES.MODE_CTR, counter=counter)
mac_str = '\0' * 16 mac_str = '\0' * 16
@ -502,11 +590,11 @@ class Mega(object):
temp_output_file.write(chunk) temp_output_file.write(chunk)
encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) encryptor = AES.new(k_str, AES.MODE_CBC, iv_str)
for i in range(0, len(chunk)-16, 16): for i in range(0, len(chunk) - 16, 16):
block = chunk[i:i + 16] block = chunk[i:i + 16]
encryptor.encrypt(block) encryptor.encrypt(block)
#fix for files under 16 bytes failing # fix for files under 16 bytes failing
if file_size > 16: if file_size > 16:
i += 16 i += 16
else: else:
@ -520,7 +608,11 @@ class Mega(object):
if self.options.get('verbose') is True: if self.options.get('verbose') is True:
# temp file size # temp file size
file_info = os.stat(temp_output_file.name) file_info = os.stat(temp_output_file.name)
print('{0} of {1} downloaded'.format(file_info.st_size, file_size)) print(
'{0} of {1} downloaded'.format(
file_info.st_size, file_size
)
)
file_mac = str_to_a32(mac_str) file_mac = str_to_a32(mac_str)
@ -535,22 +627,24 @@ class Mega(object):
########################################################################## ##########################################################################
# UPLOAD # UPLOAD
def upload(self, filename, dest=None, dest_filename=None): def upload(self, filename, dest=None, dest_filename=None):
#determine storage node # determine storage node
if dest is None: if dest is None:
#if none set, upload to cloud drive node # if none set, upload to cloud drive node
if not hasattr(self, 'root_id'): if not hasattr(self, 'root_id'):
self.get_files() self.get_files()
dest = self.root_id dest = self.root_id
#request upload url, call 'u' method # request upload url, call 'u' method
input_file = open(filename, 'rb') input_file = open(filename, 'rb')
file_size = os.path.getsize(filename) file_size = os.path.getsize(filename)
ul_url = self._api_request({'a': 'u', 's': file_size})['p'] ul_url = self._api_request({'a': 'u', 's': file_size})['p']
#generate random aes key (128) for file # generate random aes key (128) for file
ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
k_str = a32_to_str(ul_key[:4]) k_str = a32_to_str(ul_key[:4])
count = Counter.new(128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64) count = Counter.new(
128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64
)
aes = AES.new(k_str, AES.MODE_CTR, counter=count) aes = AES.new(k_str, AES.MODE_CTR, counter=count)
upload_progress = 0 upload_progress = 0
@ -565,11 +659,11 @@ class Mega(object):
upload_progress += len(chunk) upload_progress += len(chunk)
encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) encryptor = AES.new(k_str, AES.MODE_CBC, iv_str)
for i in range(0, len(chunk)-16, 16): for i in range(0, len(chunk) - 16, 16):
block = chunk[i:i + 16] block = chunk[i:i + 16]
encryptor.encrypt(block) encryptor.encrypt(block)
#fix for files under 16 bytes failing # fix for files under 16 bytes failing
if file_size > 16: if file_size > 16:
i += 16 i += 16
else: else:
@ -580,23 +674,31 @@ class Mega(object):
block += '\0' * (16 - len(block) % 16) block += '\0' * (16 - len(block) % 16)
mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) mac_str = mac_encryptor.encrypt(encryptor.encrypt(block))
#encrypt file and upload # encrypt file and upload
chunk = aes.encrypt(chunk) chunk = aes.encrypt(chunk)
output_file = requests.post(ul_url + "/" + str(chunk_start), output_file = requests.post(
data=chunk, timeout=self.timeout) ul_url + "/" + str(chunk_start),
data=chunk,
timeout=self.timeout
)
completion_file_handle = output_file.text completion_file_handle = output_file.text
if self.options.get('verbose') is True: if self.options.get('verbose') is True:
# upload progress # upload progress
print('{0} of {1} uploaded'.format(upload_progress, file_size)) print(
'{0} of {1} uploaded'.format(
upload_progress, file_size
)
)
else: else:
output_file = requests.post(ul_url + "/0", output_file = requests.post(
data='', timeout=self.timeout) ul_url + "/0", data='', timeout=self.timeout
)
completion_file_handle = output_file.text completion_file_handle = output_file.text
file_mac = str_to_a32(mac_str) file_mac = str_to_a32(mac_str)
#determine meta mac # determine meta mac
meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3])
if dest_filename is not None: if dest_filename is not None:
@ -605,68 +707,92 @@ class Mega(object):
attribs = {'n': os.path.basename(filename)} attribs = {'n': os.path.basename(filename)}
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4]))
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], key = [
ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5],
ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]] 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)) encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
#update attributes # update attributes
data = self._api_request({'a': 'p', 't': dest, 'n': [{ data = self._api_request(
'h': completion_file_handle, {
't': 0, 'a':
'a': encrypt_attribs, 'p',
'k': encrypted_key}]}) 't':
#close input file and return API msg dest,
'n': [
{
'h': completion_file_handle,
't': 0,
'a': encrypt_attribs,
'k': encrypted_key
}
]
}
)
# close input file and return API msg
input_file.close() input_file.close()
return data return data
##########################################################################
# OTHER OPERATIONS
def create_folder(self, name, dest=None): def create_folder(self, name, dest=None):
#determine storage node # determine storage node
if dest is None: if dest is None:
#if none set, upload to cloud drive node # if none set, upload to cloud drive node
if not hasattr(self, 'root_id'): if not hasattr(self, 'root_id'):
self.get_files() self.get_files()
dest = self.root_id dest = self.root_id
#generate random aes key (128) for folder # generate random aes key (128) for folder
ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
#encrypt attribs # encrypt attribs
attribs = {'n': name} attribs = {'n': name}
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4]))
encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key)) encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key))
#update attributes # update attributes
data = self._api_request({'a': 'p', data = self._api_request(
't': dest, {
'n': [{ 'a':
'h': 'xxxxxxxx', 'p',
't': 1, 't':
'a': encrypt_attribs, dest,
'k': encrypted_key} 'n': [
], {
'i': self.request_id}) 'h': 'xxxxxxxx',
#return API msg 't': 1,
'a': encrypt_attribs,
'k': encrypted_key
}
],
'i':
self.request_id
}
)
return data return data
def rename(self, file, new_name): def rename(self, file, new_name):
file = file[1] file = file[1]
#create new attribs # create new attribs
attribs = {'n': new_name} attribs = {'n': new_name}
#encrypt attribs # encrypt attribs
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k'])) encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k']))
encrypted_key = a32_to_base64(encrypt_key(file['key'], self.master_key)) encrypted_key = a32_to_base64(
encrypt_key(file['key'], self.master_key)
)
#update attributes # update attributes
data = self._api_request([{ data = self._api_request(
'a': 'a', [
'attr': encrypt_attribs, {
'key': encrypted_key, 'a': 'a',
'n': file['h'], 'attr': encrypt_attribs,
'i': self.request_id}]) 'key': encrypted_key,
'n': file['h'],
#return API msg 'i': self.request_id
}
]
)
return data return data
def move(self, file_id, target): def move(self, file_id, target):
@ -689,7 +815,7 @@ class Mega(object):
target's structure returned by find() target's structure returned by find()
""" """
#determine target_node_id # determine target_node_id
if type(target) == int: if type(target) == int:
target_node_id = str(self.get_node_by_type(target)[0]) target_node_id = str(self.get_node_by_type(target)[0])
elif type(target) in (str, unicode): elif type(target) in (str, unicode):
@ -697,10 +823,14 @@ class Mega(object):
else: else:
file = target[1] file = target[1]
target_node_id = file['h'] target_node_id = file['h']
return self._api_request({'a': 'm', return self._api_request(
'n': file_id, {
't': target_node_id, 'a': 'm',
'i': self.request_id}) 'n': file_id,
't': target_node_id,
'i': self.request_id
}
)
def add_contact(self, email): def add_contact(self, email):
""" """
@ -728,27 +858,30 @@ class Mega(object):
if not re.match(r"[^@]+@[^@]+\.[^@]+", email): if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
ValidationError('add_contact requires a valid email address') ValidationError('add_contact requires a valid email address')
else: else:
return self._api_request({'a': 'ur', return self._api_request(
'u': email, {
'l': l, 'a': 'ur',
'i': self.request_id}) 'u': email,
'l': l,
'i': self.request_id
}
)
def get_contacts(self): def get_contacts(self):
raise NotImplementedError() raise NotImplementedError()
# TODO implement this # TODO implement this
# sn param below = maxaction var with function getsc() in mega.co.nz js # sn param below = maxaction var with function getsc() in mega.co.nz js
# seens to be the 'sn' attrib of the previous request response... # seens to be the 'sn' attrib of the previous request response...
# mega.co.nz js full source @ http://homepages.shu.ac.uk/~rjodwyer/mega-scripts-all.js
# requests goto /sc rather than # requests goto /sc rather than
#req = requests.post( # req = requests.post(
#'{0}://g.api.{1}/sc'.format(self.schema, self.domain), # '{0}://g.api.{1}/sc'.format(self.schema, self.domain),
# params={'sn': 'ZMxcQ_DmHnM', 'ssl': '1'}, # params={'sn': 'ZMxcQ_DmHnM', 'ssl': '1'},
# data=json.dumps(None), # data=json.dumps(None),
# timeout=self.timeout) # timeout=self.timeout)
#json_resp = json.loads(req.text) # json_resp = json.loads(req.text)
#print json_resp # print json_resp
def get_public_url_info(self, url): def get_public_url_info(self, url):
""" """
Get size and name from a public url, dict returned Get size and name from a public url, dict returned
@ -761,18 +894,17 @@ class Mega(object):
Import the public url into user account Import the public url into user account
""" """
file_handle, file_key = self._parse_url(url).split('!') file_handle, file_key = self._parse_url(url).split('!')
return self.import_public_file(file_handle, file_key, dest_node=dest_node, dest_name=dest_name) return self.import_public_file(
file_handle, file_key, dest_node=dest_node, dest_name=dest_name
)
def get_public_file_info(self, file_handle, file_key): def get_public_file_info(self, file_handle, file_key):
""" """
Get size and name of a public file Get size and name of a public file
""" """
data = self._api_request({ data = self._api_request({'a': 'g', 'p': file_handle, 'ssm': 1})
'a': 'g',
'p': file_handle,
'ssm': 1})
#if numeric error code response # if numeric error code response
if isinstance(data, int): if isinstance(data, int):
raise RequestError(data) raise RequestError(data)
@ -780,20 +912,22 @@ class Mega(object):
raise ValueError("Unexpected result", data) raise ValueError("Unexpected result", data)
key = base64_to_a32(file_key) key = base64_to_a32(file_key)
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) k = (
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]
)
size = data['s'] size = data['s']
unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k) unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k)
if not unencrypted_attrs: if not unencrypted_attrs:
return None return None
result = { result = {'size': size, 'name': unencrypted_attrs['n']}
'size': size,
'name': unencrypted_attrs['n']}
return result return result
def import_public_file(self, file_handle, file_key, dest_node=None, dest_name=None): def import_public_file(
self, file_handle, file_key, dest_node=None, dest_name=None
):
""" """
Import the public file into user account Import the public file into user account
""" """
@ -809,19 +943,27 @@ class Mega(object):
dest_name = pl_info['name'] dest_name = pl_info['name']
key = base64_to_a32(file_key) key = base64_to_a32(file_key)
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) 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_key = a32_to_base64(encrypt_key(key, self.master_key))
encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k)) encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k))
data = self._api_request({ data = self._api_request(
'a': 'p', {
't': dest_node['h'], 'a':
'n': [{ 'p',
'ph': file_handle, 't':
't': 0, dest_node['h'],
'a': encrypted_name, 'n': [
'k': encrypted_key}]}) {
'ph': file_handle,
#return API msg 't': 0,
'a': encrypted_name,
'k': encrypted_key
}
]
}
)
return data return data

8
requirements-dev.txt Normal file
View file

@ -0,0 +1,8 @@
-r requirements.txt
pytest
ipdb
flake8
pep8-naming
autoflake
mccabe
yapf

View file

@ -1,3 +1,2 @@
requests>=0.10 requests>=0.10
pycrypto pycrypto
mega.py

28
setup.cfg Normal file
View file

@ -0,0 +1,28 @@
[bdist_wheel]
universal = 1
[zest.releaser]
create-wheel = yes
[tool:pytest]
addopts = -x -s -v
norecursedirs = .git
[flake8]
exclude = .git,__pycache__,legacy,build,dist,.tox
max-complexity = 15
ignore = E741
[yapf]
based_on_style = pep8
spaces_before_comment = 2
split_before_logical_operator = true
indent_width = 4
split_complex_comprehension = true
column_limit = 79
dedent_closing_brackets = true
spaces_around_power_operator = true
no_spaces_around_selected_binary_operators = false
split_penalty_import_names = 500
join_multiple_lines = true

View file

@ -9,25 +9,31 @@ def get_packages(package):
""" """
Return root package & all sub-packages. Return root package & all sub-packages.
""" """
return [dirpath return [
for dirpath, dirnames, filenames in os.walk(package) dirpath for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, '__init__.py'))] if os.path.exists(os.path.join(dirpath, '__init__.py'))
]
def get_package_data(package): def get_package_data(package):
""" """
Return all files under the root package, that are not in a Return all files under the root package, that are not in a
package themselves. package themselves.
""" """
walk = [(dirpath.replace(package + os.sep, '', 1), filenames) walk = [
for dirpath, dirnames, filenames in os.walk(package) (dirpath.replace(package + os.sep, '', 1), filenames)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))] for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))
]
filepaths = [] filepaths = []
for base, filenames in walk: for base, filenames in walk:
filepaths.extend([os.path.join(base, filename) filepaths.extend(
for filename in filenames]) [os.path.join(base, filename) for filename in filenames]
)
return {package: filepaths} return {package: filepaths}
setup( setup(
name='mega.py', name='mega.py',
version='0.9.17', version='0.9.17',
@ -41,8 +47,7 @@ setup(
install_requires=['pycrypto', 'requests'], install_requires=['pycrypto', 'requests'],
classifiers=[ classifiers=[
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Operating System :: OS Independent', 'Operating System :: OS Independent', 'Programming Language :: Python',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP' 'Topic :: Internet :: WWW/HTTP'
] ]
) )

View file

@ -3,23 +3,26 @@ These unit tests will upload a test file,a test folder and a test contact,
Perform api operations on them, Perform api operations on them,
And them remove them from your account. And them remove them from your account.
""" """
from mega import Mega
import unittest import unittest
import random import random
import os import os
email = 'your@email.com' from mega import Mega
password = 'password'
email = os.environ['EMAIL']
password = os.environ['PASS']
mega = Mega() mega = Mega()
# anonymous login # anonymous login
m = mega.login() m = mega.login()
# normal login # normal login
#m = mega.login(email, password) # m = mega.login(email, password)
FIND_RESP = None FIND_RESP = None
TEST_CONTACT = 'test@mega.co.nz' TEST_CONTACT = 'test@mega.co.nz'
TEST_PUBLIC_URL = 'https://mega.co.nz/#!EYI2VagT!Ic1yblki8oM4v6XHquCe4gu84kxc4glFchj8OvcT5lw' TEST_PUBLIC_URL = (
'https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps'
)
TEST_FILE = os.path.basename(__file__) TEST_FILE = os.path.basename(__file__)
TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random()) TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random())
@ -88,7 +91,7 @@ class TestMega(unittest.TestCase):
self.assertIsInstance(resp, int) self.assertIsInstance(resp, int)
def test_empty_trash(self): def test_empty_trash(self):
#resp None if already empty, else int # resp None if already empty, else int
resp = m.empty_trash() resp = m.empty_trash()
if resp is not None: if resp is not None:
self.assertIsInstance(resp, int) self.assertIsInstance(resp, int)