First commit
This commit is contained in:
commit
bc70a59bbe
6 changed files with 468 additions and 0 deletions
1
README
Normal file
1
README
Normal file
|
@ -0,0 +1 @@
|
|||
This file was created by PyCharm 2.6.3 for binding GitHub repository
|
46
README.md
Normal file
46
README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Mega.py
|
||||
|
||||
Python library for the Mega.co.nz API, currently supporting login, uploading & downloading of files.
|
||||
|
||||
Further functionality coming soon.
|
||||
|
||||
## How To Use
|
||||
|
||||
### Create an instance of Mega.py
|
||||
|
||||
mega = Mega()
|
||||
|
||||
### Login to Mega
|
||||
|
||||
m = mega.login(email, password)
|
||||
|
||||
### Get account files
|
||||
|
||||
files = m.get_files()
|
||||
|
||||
### Upload a file
|
||||
|
||||
m.upload('myfile.doc')
|
||||
|
||||
### Download a file from URL
|
||||
|
||||
m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Python2.7+
|
||||
2. Python requests - python-requests.org
|
||||
|
||||
## Tests
|
||||
|
||||
Test .py files can be found in /tests, run these to ensure Mega.py is working 100%.
|
||||
|
||||
## Contribute
|
||||
|
||||
Feel free to pull the source and make changes and additions.
|
||||
|
||||
Learn about the API at Mega.co.nz
|
||||
- https://mega.co.nz/#developers
|
||||
|
||||
|
130
crypto.py
Normal file
130
crypto.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
from Crypto.Cipher import AES
|
||||
import json
|
||||
import base64
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
def aes_cbc_encrypt(data, key):
|
||||
aes_cipher = AES.new(key, AES.MODE_CBC, '\0' * 16)
|
||||
return aes_cipher.encrypt(data)
|
||||
|
||||
def aes_cbc_decrypt(data, key):
|
||||
aes_cipher = AES.new(key, AES.MODE_CBC, '\0' * 16)
|
||||
return aes_cipher.decrypt(data)
|
||||
|
||||
def aes_cbc_encrypt_a32(data, key):
|
||||
return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key)))
|
||||
|
||||
def aes_cbc_decrypt_a32(data, key):
|
||||
return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
|
||||
|
||||
def stringhash(str, aeskey):
|
||||
s32 = str_to_a32(str)
|
||||
h32 = [0, 0, 0, 0]
|
||||
for i in range(len(s32)):
|
||||
h32[i % 4] ^= s32[i]
|
||||
for r in range(0x4000):
|
||||
h32 = aes_cbc_encrypt_a32(h32, aeskey)
|
||||
return a32_to_base64((h32[0], h32[2]))
|
||||
|
||||
def prepare_key(arr):
|
||||
pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
|
||||
for r in range(0x10000):
|
||||
for j in range(0, len(arr), 4):
|
||||
key = [0, 0, 0, 0]
|
||||
for i in range(4):
|
||||
if i + j < len(arr):
|
||||
key[i] = arr[i + j]
|
||||
pkey = aes_cbc_encrypt_a32(pkey, key)
|
||||
return pkey
|
||||
|
||||
|
||||
def encrypt_key(a, key):
|
||||
return sum(
|
||||
(aes_cbc_encrypt_a32(a[i:i+4], key)
|
||||
for i in range(0, len(a), 4)), ())
|
||||
|
||||
|
||||
def decrypt_key(a, key):
|
||||
return sum(
|
||||
(aes_cbc_decrypt_a32(a[i:i+4], key)
|
||||
for i in range(0, len(a), 4)), ())
|
||||
|
||||
|
||||
def encrypt_attr(attr, key):
|
||||
attr = 'MEGA' + json.dumps(attr)
|
||||
if len(attr) % 16:
|
||||
attr += '\0' * (16 - len(attr) % 16)
|
||||
return aes_cbc_encrypt(attr, a32_to_str(key))
|
||||
|
||||
|
||||
def decrypt_attr(attr, key):
|
||||
attr = aes_cbc_decrypt(attr, a32_to_str(key)).rstrip('\0')
|
||||
return json.loads(attr[4:])
|
||||
|
||||
|
||||
def a32_to_str(a):
|
||||
return struct.pack('>%dI' % len(a), *a)
|
||||
|
||||
def aes_cbc_encrypt(data, key):
|
||||
aes = AES.new(key, AES.MODE_CBC, '\0' * 16)
|
||||
return aes.encrypt(data)
|
||||
|
||||
def aes_cbc_encrypt_a32(data, key):
|
||||
return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key)))
|
||||
|
||||
def str_to_a32(b):
|
||||
if len(b) % 4:
|
||||
# pad to multiple of 4
|
||||
b += '\0' * (4 - len(b) % 4)
|
||||
return struct.unpack('>%dI' % (len(b) / 4), b)
|
||||
|
||||
def mpi_to_int(s):
|
||||
return int(binascii.hexlify(s[2:]), 16)
|
||||
|
||||
def aes_cbc_decrypt(data, key):
|
||||
decryptor = AES.new(key, AES.MODE_CBC, '\0' * 16)
|
||||
return decryptor.decrypt(data)
|
||||
|
||||
def aes_cbc_decrypt_a32(data, key):
|
||||
return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
|
||||
|
||||
def base64_url_decode(data):
|
||||
data += '=='[(2 - len(data) * 3) % 4:]
|
||||
for search, replace in (('-', '+'), ('_', '/'), (',', '')):
|
||||
data = data.replace(search, replace)
|
||||
return base64.b64decode(data)
|
||||
|
||||
def base64_to_a32(s):
|
||||
return str_to_a32(base64_url_decode(s))
|
||||
|
||||
def base64_url_encode(data):
|
||||
data = base64.b64encode(data)
|
||||
for search, replace in (('+', '-'), ('/', '_'), ('=', '')):
|
||||
data = data.replace(search, replace)
|
||||
return data
|
||||
|
||||
def a32_to_base64(a):
|
||||
return base64_url_encode(a32_to_str(a))
|
||||
|
||||
def get_chunks(size):
|
||||
chunks = {}
|
||||
p = pp = 0
|
||||
i = 1
|
||||
|
||||
while i <= 8 and p < size - i * 0x20000:
|
||||
chunks[p] = i * 0x20000
|
||||
pp = p
|
||||
p += chunks[p]
|
||||
i += 1
|
||||
|
||||
while p < size:
|
||||
chunks[p] = 0x100000
|
||||
pp = p
|
||||
p += chunks[p]
|
||||
|
||||
chunks[pp] = size - pp
|
||||
if not chunks[pp]:
|
||||
del chunks[pp]
|
||||
|
||||
return chunks
|
14
errors.py
Normal file
14
errors.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
class ValidationError(Exception):
|
||||
"""
|
||||
Error in validation stage
|
||||
"""
|
||||
pass
|
||||
|
||||
class RequestError(Exception):
|
||||
"""
|
||||
Error in API request
|
||||
"""
|
||||
#TODO add error response messages
|
||||
pass
|
||||
|
251
mega.py
Normal file
251
mega.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
import re
|
||||
import json
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util import Counter
|
||||
import os
|
||||
import random
|
||||
import binascii
|
||||
import requests
|
||||
import errors
|
||||
from crypto import *
|
||||
|
||||
class Mega(object):
|
||||
|
||||
def __init__(self):
|
||||
self.schema = 'https'
|
||||
self.domain = 'mega.co.nz'
|
||||
self.timeout = 160 #max time (secs) to wait for response from api requests
|
||||
self.sid = None
|
||||
self.sequence_num = random.randint(0, 0xFFFFFFFF)
|
||||
|
||||
@classmethod
|
||||
def login(class_, email, password):
|
||||
inst = class_()
|
||||
inst.login_user(email, password)
|
||||
return inst
|
||||
|
||||
|
||||
def login_user(self, email, password):
|
||||
password_aes = prepare_key(str_to_a32(password))
|
||||
uh = stringhash(email, password_aes)
|
||||
resp = self.api_request({'a': 'us', 'user': email, 'uh': uh})
|
||||
#if numeric error code response
|
||||
if isinstance(resp, int):
|
||||
raise errors.RequestError(resp)
|
||||
self._login_process(resp, password_aes)
|
||||
|
||||
|
||||
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 = ((ord(private_key[0]) * 256 + ord(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],
|
||||
0L, self.rsa_private_key[2], self.rsa_private_key[0],
|
||||
self.rsa_private_key[1]))
|
||||
|
||||
sid = '%x' % rsa_decrypter.key._decrypt(encrypted_sid)
|
||||
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
|
||||
self.sid = base64_url_encode(sid[:43])
|
||||
|
||||
def api_request(self, data):
|
||||
params = {'id': self.sequence_num}
|
||||
self.sequence_num += 1
|
||||
|
||||
if self.sid:
|
||||
params.update({'sid': self.sid})
|
||||
req = requests.post(
|
||||
'{0}://g.api.{1}/cs'.format(self.schema,self.domain), params=params, data=json.dumps([data]), timeout=self.timeout)
|
||||
json_resp = req.json()
|
||||
#if numeric error code response
|
||||
if isinstance(json_resp, int):
|
||||
raise errors.RequestError(json_resp)
|
||||
return json_resp[0]
|
||||
|
||||
def get_files(self):
|
||||
files = self.api_request({'a': 'f', 'c': 1})
|
||||
files_dict = {}
|
||||
for file in files['f']:
|
||||
files_dict[file['h']] = self.process_file(file)
|
||||
return files_dict
|
||||
|
||||
|
||||
def download_url(self, url):
|
||||
path = self.parse_url(url).split('!')
|
||||
file_id = path[0]
|
||||
file_key = path[1]
|
||||
self.download_file(file_id, file_key, is_public=True)
|
||||
|
||||
def parse_url(self, url):
|
||||
#parse file id and key from url
|
||||
if('!' in url):
|
||||
match = re.findall(r'/#!(.*)', url)
|
||||
path = match[0]
|
||||
return path
|
||||
else:
|
||||
raise errors.RequestError('Url key missing')
|
||||
|
||||
def download_file(self, file_id, file_key, is_public=False):
|
||||
if is_public:
|
||||
file_key = base64_to_a32(file_key)
|
||||
file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_id})
|
||||
else:
|
||||
file_data = self.api_request({'a': 'g', 'g': 1, 'n': file_id})
|
||||
|
||||
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]
|
||||
|
||||
file_url = file_data['g']
|
||||
file_size = file_data['s']
|
||||
attribs = base64_url_decode(file_data['at'])
|
||||
attribs = decrypt_attr(attribs, k)
|
||||
file_name = attribs['n']
|
||||
|
||||
input_file = requests.get(file_url, stream=True).raw
|
||||
output_file = open(file_name, 'wb')
|
||||
|
||||
counter = Counter.new(
|
||||
128, initial_value=((iv[0] << 32) + iv[1]) << 64)
|
||||
aes = AES.new(a32_to_str(k), AES.MODE_CTR, counter=counter)
|
||||
|
||||
file_mac = (0, 0, 0, 0)
|
||||
for chunk_start, chunk_size in sorted(get_chunks(file_size).items()):
|
||||
chunk = input_file.read(chunk_size)
|
||||
chunk = aes.decrypt(chunk)
|
||||
output_file.write(chunk)
|
||||
|
||||
chunk_mac = [iv[0], iv[1], iv[0], iv[1]]
|
||||
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, k)
|
||||
|
||||
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, k)
|
||||
output_file.close()
|
||||
|
||||
# check mac integrity
|
||||
if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac:
|
||||
raise ValueError('Mismatched mac')
|
||||
|
||||
def get_public_url(self, file_id, file_key):
|
||||
if file_id and file_key:
|
||||
public_handle = self.api_request({'a': 'l', 'n': file_id})
|
||||
decrypted_key = a32_to_base64(file_key)
|
||||
return '{0}://{1}/#!%s!%s'.format(self.schema, self.domain) % (public_handle, decrypted_key)
|
||||
else:
|
||||
raise errors.ValidationError('File id and key must be set')
|
||||
|
||||
def upload(self, filename, dest=None):
|
||||
#determine storage node
|
||||
if dest is None:
|
||||
#if none set, upload to cloud drive node
|
||||
root_id = getattr(self, 'root_id')
|
||||
if root_id is None:
|
||||
self.get_files()
|
||||
dest = self.root_id
|
||||
|
||||
#request upload url, call 'u' method
|
||||
input_file = open(filename, 'rb')
|
||||
size = os.path.getsize(filename)
|
||||
ul_url = self.api_request({'a': 'u', 's': size})['p']
|
||||
|
||||
#generate random aes key (128) for file
|
||||
ul_key = [random.randint(0, 0xFFFFFFFF) for r in range(6)]
|
||||
count = Counter.new(128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64)
|
||||
aes = AES.new(a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=count)
|
||||
|
||||
file_mac = [0, 0, 0, 0]
|
||||
for chunk_start, chunk_size in sorted(get_chunks(size).items()):
|
||||
chunk = input_file.read(chunk_size)
|
||||
|
||||
#determine chunks mac
|
||||
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])
|
||||
|
||||
#our files mac
|
||||
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])
|
||||
|
||||
#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
|
||||
|
||||
#determine meta mac
|
||||
meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3])
|
||||
|
||||
attribs = {'n': os.path.basename(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))
|
||||
#update attributes
|
||||
data = self.api_request({'a': 'p', 't': dest, 'n': [
|
||||
{'h': completion_file_handle, 't': 0, 'a': encrypt_attribs, 'k': encrypted_key}]}
|
||||
)
|
||||
return data
|
||||
|
||||
def process_file(self, file):
|
||||
if file['t'] == 0 or file['t'] == 1:
|
||||
key = file['k'][file['k'].index(':') + 1:]
|
||||
key = decrypt_key(base64_to_a32(key), self.master_key)
|
||||
if file['t'] == 0:
|
||||
k = file['k'] = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7])
|
||||
iv = file['iv'] = key[4:6] + (0, 0)
|
||||
meta_mac = file['meta_mac'] = key[6:8]
|
||||
else:
|
||||
k = file['k'] = key
|
||||
attributes = base64_url_decode(file['a'])
|
||||
attributes = decrypt_attr(attributes, k)
|
||||
file['a'] = attributes
|
||||
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
|
26
tests/test.py
Normal file
26
tests/test.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from mega import Mega
|
||||
|
||||
def test():
|
||||
|
||||
#user details
|
||||
email = 'your@email.com'
|
||||
password = 'password'
|
||||
|
||||
mega = Mega()
|
||||
|
||||
#login
|
||||
m = mega.login(email, password)
|
||||
|
||||
#get account files
|
||||
files = m.get_files()
|
||||
print(files)
|
||||
|
||||
#upload file
|
||||
print(m.upload('tests/test.py'))
|
||||
|
||||
#download file from url
|
||||
m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
Loading…
Reference in a new issue