1 #! /usr/bin/env python3
3 from urllib
.parse
import urlsplit
4 from base64
import urlsafe_b64decode
5 from net
import request_cached
9 from shutil
import copyfileobj
11 from shorthand
import bitmask
12 from contextlib
import ExitStack
15 from io
import SEEK_CUR
16 from datacopy
import TerminalLog
, Progress
17 from io
import TextIOWrapper
20 from net
import header_list
22 def main(url
, start
='0', *, v
=False, n
=False):
24 API
= 'https://g.api.mega.co.nz/'
25 if megan
.folder
is None:
26 [url
, headers
, body
] = megan
.get_file_request()
28 [url
, headers
, body
] = megan
.get_folder_request()
29 with
ExitStack() as cleanup
:
30 [header
, resp
] = request_cached(API
+ url
,
31 headers
=headers
, types
=('application/json',),
32 data
=body
, cleanup
=cleanup
)
33 megan
.handle_folder_response(header
, resp
.read())
35 [megan
.key
, req
] = megan
.get_folder_file_request(megan
.file)
36 [url
, headers
, body
] = req
37 with
ExitStack() as cleanup
:
38 [header
, resp
] = request_cached(API
+ url
,
39 headers
=headers
, types
=('application/json',),
40 data
=body
, cleanup
=cleanup
)
41 [K
, b
] = megan
.handle_file_response(header
, resp
.read())
42 print('Encryption key:', K
)
44 print('Filename:', megan
.name
)
45 print('Size: {}'.format(format_size(megan
.resp
['s'])))
50 print('Starting on block boundary at +0x{:X}'.format(start
))
52 print('Download URL:', megan
.resp
['g'])
54 print('IV:', megan
.key
[16:24].hex().upper())
55 print('MAC:', megan
.key
[24:32].hex().upper())
59 req
= urllib
.request
.Request(megan
.resp
['g'])
60 with
ExitStack() as cleanup
:
62 name
= '{}+0x{:X}'.format(megan
.name
, start
)
63 print('Writing to', name
)
64 req
.add_header('Range', 'bytes={}-'.format(start
))
65 download
= cleanup
.enter_context(urllib
.request
.urlopen(req
))
66 file = cleanup
.enter_context(open(name
, 'xb'))
67 iv
= (b
& ~
0 << 64) + (start
// 16 & bitmask(128))
68 decryptor
= make_encryptor('-aes-128-ctr', '-d', K
, iv
,
70 cleanup
.enter_context(decryptor
)
72 copyfileobj(download
, decryptor
.stdin
)
73 assert not decryptor
.wait()
75 chunk_iv
= b
& ~
0 << 64 | b
>> 64
76 authenticator
= Authenticator(megan
.resp
['s'], K
, chunk_iv
)
77 cleanup
.callback(authenticator
.close
)
79 file = cleanup
.enter_context(open(megan
.name
, 'xb'))
80 except FileExistsError
:
81 file = cleanup
.enter_context(open(megan
.name
, 'r+b'))
83 data
= file.read(0x10000)
84 if len(data
) < 0x10000:
86 authenticator
.update(data
)
87 if authenticator
.pos
+ len(data
) == megan
.resp
['s']:
88 authenticator
.update(data
)
90 limit
= len(data
) - len(data
) % 16
91 with
memoryview(data
) as view
, view
[:limit
] as slice:
92 authenticator
.update(slice)
93 file.seek(-len(data
) % 16, SEEK_CUR
)
94 pos
= authenticator
.pos
95 print('Continuing file from {}'.format(format_size(pos
)))
96 req
.add_header('Range', 'bytes={}-'.format(pos
))
97 if authenticator
.pos
< megan
.resp
['s']:
98 download
= cleanup
.enter_context(urllib
.request
.urlopen(req
))
99 iv
= (b
& ~
0 << 64) + (authenticator
.pos
// 16 & bitmask(128))
100 decryptor
= make_encryptor('-aes-128-ctr', '-d', K
, iv
,
101 stdout
=subprocess
.PIPE
)
102 cleanup
.enter_context(decryptor
)
103 selector
= cleanup
.enter_context(selectors
.DefaultSelector())
106 download_pos
= 0x10000
109 cleanup
.callback(Progress
.close
, log
)
110 progress
= Progress(log
, megan
.resp
['s'])
114 nonlocal download_buf
, download_pos
116 if download_pos
>= len(download_buf
):
117 if download_pos
< 0x10000:
119 download_buf
= download
.read(0x10000)
124 selector
.unregister(decryptor
.stdin
)
125 decryptor
.stdin
.close()
127 with
memoryview(download_buf
) as view
, \
128 view
[download_pos
:] as rest
:
129 n
= decryptor
.stdin
.write(rest
)
134 progress
.update(total_dl
)
135 os
.set_blocking(decryptor
.stdin
.fileno(), False)
136 selector
.register(decryptor
.stdin
, selectors
.EVENT_WRITE
,
140 data
= decryptor
.stdout
.read(0x10000)
144 authenticator
.update(data
)
145 selector
.register(decryptor
.stdout
, selectors
.EVENT_READ
,
148 while authenticator
.pos
< megan
.resp
['s']:
149 for [k
, events
] in selector
.select():
151 assert hmac
.compare_digest(authenticator
.digest(), megan
.key
[24:])
152 print('MAC validated')
155 def __init__(self
, url
):
156 split
= urlsplit(url
)
157 assert split
.netloc
== 'mega.nz'
158 assert split
.scheme
== 'https'
159 assert not split
.query
161 if split
.path
== '/':
163 assert split
.fragment
.startswith('!')
164 [self
.id, self
.key
] = split
.fragment
[1:].split('!', 2)
165 self
.key
= base64_decode(self
.key
, 256)
168 assert split
.path
.startswith('/folder/')
169 self
.folder
= split
.path
[len('/folder/'):]
170 self
.key
= split
.fragment
.split('/file/', 2)
171 if len(self
.key
) == 1:
172 [self
.key
] = self
.key
175 [self
.key
, self
.file] = self
.key
176 self
.key
= base64_decode(self
.key
, 128).hex()
179 def get_folder_request(self
):
180 return make_request('f', {
183 }, f
'?n={self.folder}')
185 def handle_folder_response(self
, *folder
):
186 folder
= decode_response(*folder
)
188 for f
in folder
['f']:
190 'key': base64_decode(f
['k'].split(':', 1)[1]),
192 'type': NodeType(f
['t']),
196 existing
= self
.nodes
.setdefault(f
['h'], node
)
197 assert existing
is node
198 if node
['type'] is NodeType
.FILE
:
199 node
['size'] = f
['s']
200 self
.root
= folder
['f'][0]['h']
201 node
= self
.nodes
[self
.root
]
202 [k
, b
] = split_key(self
.decrypt_key(node
))
203 self
.name
= decrypt_name(node
['a'], k
)
205 def get_folder_file_request(self
, file):
206 key
= self
.decrypt_key(self
.nodes
[file])
210 'g': 1, # Include download URL
212 return (key
, make_request('g', args
, f
'?n={self.folder}'))
214 def get_file_request(self
):
217 'g': 1, # Include download URL
219 return make_request('g', args
)
221 def handle_file_response(self
, *resp
):
222 self
.resp
= decode_response(*resp
)
224 self
.nodes
= {None: {
225 'type': NodeType
.FILE
,
226 'a': self
.resp
['at'],
227 'size': self
.resp
['s'],
230 [K
, b
] = split_key(self
.key
)
231 self
.name
= decrypt_name(self
.resp
['at'], K
)
234 def decrypt_key(self
, node
):
235 with
make_encryptor('-aes-128-ecb', '-d', self
.key
,
236 stdout
=subprocess
.PIPE
) as dec
:
237 [k
, err
] = dec
.communicate(node
['key'])
238 assert dec
.returncode
== 0
242 a
= int.from_bytes(key
[:16], 'big')
243 b
= int.from_bytes(key
[16:], 'big')
244 return (format(a ^ b
, '032X'), b
)
246 def decrypt_name(a
, k
):
247 dec
= make_encryptor('-aes-128-cbc', '-d', k
, 0, stdout
=subprocess
.PIPE
)
249 [a
, err
] = dec
.communicate(base64_decode(a
))
250 assert dec
.returncode
== 0
251 assert a
.startswith(b
'MEGA')
252 a
= a
[4:].rstrip(b
'\x00')
253 a
= json
.loads(a
.decode('utf-8'))
256 class NodeType(Enum
):
260 def make_request(type, args
, url
=''):
262 return ('cs' + url
, (
263 ('User-Agent', 'megan'),
264 ('Content-Type', 'application/json'),
265 ('Accept-Encoding', 'gzip, x-gzip'),
266 ), json
.dumps((args
,)).encode('ascii'))
268 def decode_response(header
, resp
):
270 for code
in header_list(header
, 'Content-Encoding'):
271 if code
.lower() in {'gzip', 'x-gzip'}:
273 raise TypeError('Recursive gzip encoding')
274 resp
= gzip
.decompress(resp
)
277 raise TypeError(f
'Unhandled encoding: {code!r}')
279 resp
= json
.loads(resp
)
280 if not isinstance(resp
, list):
283 if not isinstance(resp
, dict):
290 -3: 'AGAIN: Temporary congestion',
293 -6: 'TOOMANY: Connection limit exceeded or terms breached',
301 -14: 'KEY: Decryption failed',
302 -15: 'SID: Invalid session',
306 -19: 'TOOMANYCONNECTIONS',
312 raise Exception(f
'{ERRORS[code]} ({code})')
314 def base64_decode(s
, bits
=None):
316 assert len(s
) == ceildiv(bits
, 6)
317 return urlsafe_b64decode(s
+ '=' * (-len(s
) % 4))
319 class Authenticator(ExitStack
):
320 def __init__(self
, size
, K
, chunk_iv
):
321 ExitStack
.__init
__(self
)
324 self
.whole_mac
= self
.enter_context(CbcMac(self
.K
))
327 self
.chunk_mac
= None
328 self
.chunk_cleanup
= self
.enter_context(ExitStack())
330 self
.padding
= -self
.size
% 16
331 self
.end
= self
.size
+ self
.padding
332 self
.chunk_iv
= chunk_iv
334 def update(self
, data
):
335 with
memoryview(data
) as view
:
338 if not self
.chunk_mac
:
340 self
.chunk_mac
= CbcMac(self
.K
, self
.chunk_iv
)
341 self
.chunk_cleanup
.callback(self
.chunk_mac
.close
)
342 if self
.chunk_size
< 1 << 20:
343 self
.chunk_size
+= 0x20000
345 chunk_end
= pos
+ min(self
.chunk_size
- self
.chunk_pos
,
347 with view
[pos
:max(chunk_end
- 16, 0)] as early
:
348 self
.chunk_mac
.write(early
)
350 self
.chunk_pos
+= len(early
)
351 self
.pos
+= len(early
)
352 if pos
< chunk_end
- 16:
354 block
= view
[pos
:chunk_end
]
355 self
.chunk_mac
.write_last_block(block
)
357 self
.chunk_pos
+= len(block
)
358 self
.pos
+= len(block
)
359 if self
.pos
== self
.size
:
360 self
.chunk_mac
.write_last_block(bytes(self
.padding
))
361 self
.whole_mac
.write_last_block(self
.chunk_mac
.get_mac())
365 self
.whole_mac
.write(self
.chunk_mac
.get_mac())
366 self
.chunk_cleanup
.close()
367 self
.chunk_mac
= None
370 mac
= int.from_bytes(self
.whole_mac
.get_mac(), 'big')
372 mac
= mac
.to_bytes(16, 'big')
373 return mac
[4:8] + mac
[12:16]
375 class CbcMac(ExitStack
):
376 def __init__(self
, K
, iv
=0):
377 ExitStack
.__init
__(self
)
378 self
.proc
= make_encryptor('-aes-128-cbc', '-e', K
, iv
,
379 stdout
=subprocess
.PIPE
)
380 self
.enter_context(self
.proc
)
381 self
.selector
= self
.enter_context(selectors
.DefaultSelector())
382 os
.set_blocking(self
.proc
.stdin
.fileno(), False)
383 self
.selector
.register(self
.proc
.stdin
, selectors
.EVENT_WRITE
,
385 self
.selector
.register(self
.proc
.stdout
, selectors
.EVENT_READ
,
389 def write(self
, data
):
390 self
.to_skip
+= len(data
)
391 self
.write_last_block(data
)
394 with self
.data
[self
.data_pos
:] as data
:
395 self
.data_pos
+= self
.proc
.stdin
.write(data
)
398 chunk
= min(self
.to_skip
, 0x10000)
399 self
.to_skip
-= len(self
.proc
.stdout
.read(chunk
))
401 def write_last_block(self
, data
):
402 with
memoryview(data
) as self
.data
:
404 while self
.data_pos
< len(data
):
405 for [key
, events
] in self
.selector
.select():
409 self
.proc
.stdin
.close()
412 return self
.proc
.stdout
.readall()
414 def make_encryptor(cipher
, dir, K
, iv
=None, *, stdout
):
415 args
= ['openssl', 'enc', cipher
, '-nopad', dir, '-K', K
]
417 args
.extend(('-iv', format(iv
, '032X')))
418 return subprocess
.Popen(args
,
419 stdin
=subprocess
.PIPE
, stdout
=stdout
, bufsize
=0)
423 [prefix
, scaled_exp
] = divmod(len(si
) - 1, 3)
425 whole
= si
[:1 + scaled_exp
]
426 fraction
= si
[1 + scaled_exp
:]
427 prefix
= 'kMGTPE'[prefix
- 1]
428 si
= '{}.{} {}B'.format(whole
, fraction
, prefix
)
429 prefix
= (s
.bit_length() - 1) // 10
430 bin
= s
/ 1024**prefix
431 digits
= format(bin
, '#.3g')
435 digits
= format(bin
, '#.3g')
436 prefix
= 'KMGTPE'[prefix
- 1]
437 size
= '{} ({} {}iB)'.format(si
, digits
.rstrip('.'), prefix
)
445 if __name__
== "__main__":