commit bc70a59bbe6276dc84d1c030ce194ac769e9d03f Author: richard@richard.do Date: Mon Feb 4 02:02:33 2013 +0000 First commit diff --git a/README b/README new file mode 100644 index 0000000..4aea597 --- /dev/null +++ b/README @@ -0,0 +1 @@ +This file was created by PyCharm 2.6.3 for binding GitHub repository \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1d3f93 --- /dev/null +++ b/README.md @@ -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 + + diff --git a/crypto.py b/crypto.py new file mode 100644 index 0000000..d1d30c1 --- /dev/null +++ b/crypto.py @@ -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 diff --git a/errors.py b/errors.py new file mode 100644 index 0000000..e6d58c6 --- /dev/null +++ b/errors.py @@ -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 + diff --git a/mega.py b/mega.py new file mode 100644 index 0000000..ea9257a --- /dev/null +++ b/mega.py @@ -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 \ No newline at end of file diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..453bfff --- /dev/null +++ b/tests/test.py @@ -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() \ No newline at end of file