rmega 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/TODO.md +3 -5
- data/bin/rmega-dl +47 -0
- data/bin/rmega-up +31 -0
- data/lib/rmega.rb +35 -3
- data/lib/rmega/api_response.rb +80 -0
- data/lib/rmega/cli.rb +121 -0
- data/lib/rmega/crypto.rb +20 -0
- data/lib/rmega/crypto/aes_cbc.rb +46 -0
- data/lib/rmega/crypto/aes_ctr.rb +15 -84
- data/lib/rmega/crypto/aes_ecb.rb +25 -0
- data/lib/rmega/crypto/rsa.rb +21 -12
- data/lib/rmega/errors.rb +3 -51
- data/lib/rmega/loggable.rb +0 -3
- data/lib/rmega/net.rb +56 -0
- data/lib/rmega/nodes/deletable.rb +0 -3
- data/lib/rmega/nodes/downloadable.rb +73 -30
- data/lib/rmega/nodes/expandable.rb +14 -10
- data/lib/rmega/nodes/factory.rb +30 -17
- data/lib/rmega/nodes/file.rb +0 -4
- data/lib/rmega/nodes/folder.rb +4 -14
- data/lib/rmega/nodes/inbox.rb +0 -2
- data/lib/rmega/nodes/node.rb +48 -25
- data/lib/rmega/nodes/node_key.rb +44 -0
- data/lib/rmega/nodes/root.rb +0 -4
- data/lib/rmega/nodes/trash.rb +0 -3
- data/lib/rmega/nodes/uploadable.rb +42 -33
- data/lib/rmega/not_inspectable.rb +10 -0
- data/lib/rmega/options.rb +22 -5
- data/lib/rmega/pool.rb +18 -7
- data/lib/rmega/progress.rb +53 -13
- data/lib/rmega/session.rb +125 -52
- data/lib/rmega/storage.rb +25 -21
- data/lib/rmega/utils.rb +23 -183
- data/lib/rmega/version.rb +2 -1
- data/rmega.gemspec +3 -5
- data/spec/integration/file_download_spec.rb +14 -32
- data/spec/integration/file_integrity_spec.rb +41 -0
- data/spec/integration/file_upload_spec.rb +11 -57
- data/spec/integration/folder_download_spec.rb +17 -0
- data/spec/integration/folder_operations_spec.rb +30 -30
- data/spec/integration/login_spec.rb +3 -3
- data/spec/integration/resume_download_spec.rb +53 -0
- data/spec/integration_spec_helper.rb +9 -4
- data/spec/rmega/lib/cli_spec.rb +12 -0
- data/spec/rmega/lib/session_spec.rb +31 -0
- data/spec/rmega/lib/storage_spec.rb +27 -0
- data/spec/rmega/lib/utils_spec.rb +16 -78
- data/spec/spec_helper.rb +1 -4
- metadata +30 -40
- data/lib/rmega/crypto/aes.rb +0 -35
- data/lib/rmega/crypto/crypto.rb +0 -107
- data/lib/rmega/crypto/rsa_mega.js +0 -455
- data/spec/rmega/lib/crypto/aes_spec.rb +0 -12
- data/spec/rmega/lib/crypto/crypto_spec.rb +0 -27
data/lib/rmega/crypto/aes_ctr.rb
CHANGED
@@ -1,94 +1,25 @@
|
|
1
|
-
require 'rmega/utils'
|
2
|
-
require 'rmega/crypto/aes'
|
3
|
-
|
4
1
|
module Rmega
|
5
2
|
module Crypto
|
6
3
|
module AesCtr
|
7
|
-
|
8
|
-
|
9
|
-
def decrypt(key, nonce, data)
|
10
|
-
raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
|
11
|
-
raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
|
12
|
-
|
13
|
-
nonce = nonce.dup
|
14
|
-
|
15
|
-
mac = [nonce[0], nonce[1], nonce[0], nonce[1]]
|
16
|
-
enc = nil
|
17
|
-
a32 = Utils.str_to_a32 data
|
18
|
-
len = a32.size - 3
|
19
|
-
last_i = 0
|
20
|
-
|
21
|
-
(0..len).step(4) do |i|
|
22
|
-
enc = Aes.encrypt key, nonce
|
23
|
-
4.times do |m|
|
24
|
-
a32[i+m] = (a32[i+m] || 0) ^ (enc[m] || 0)
|
25
|
-
mac[m] = (mac[m] || 0) ^ (a32[i+m] || 0)
|
26
|
-
end
|
27
|
-
mac = Aes.encrypt key, mac
|
28
|
-
nonce[3] += 1
|
29
|
-
nonce[2] += 1 if nonce[3] == 0
|
30
|
-
last_i = i + 4
|
31
|
-
end
|
32
|
-
|
33
|
-
if last_i < a32.size
|
34
|
-
v = [0, 0, 0, 0]
|
35
|
-
(last_i..a32.size - 1).step(1) { |m| v[m-last_i] = a32[m] || 0 }
|
36
|
-
|
37
|
-
enc = Aes.encrypt key, nonce
|
38
|
-
4.times { |m| v[m] = v[m] ^ enc[m] }
|
39
|
-
|
40
|
-
j = data.size & 15
|
41
|
-
m = Utils.str_to_a32 Array.new(j+1).join(255.chr)+Array.new(17-j).join(0.chr)
|
42
|
-
|
43
|
-
4.times { |x| mac[x] = mac[x] ^ (v[x] & m[x]) }
|
44
|
-
|
45
|
-
mac = Aes.encrypt key, mac
|
46
|
-
|
47
|
-
(last_i..a32.size - 1).step(1) { |j| a32[j] = v[j - last_i] || 0 }
|
48
|
-
end
|
49
|
-
|
50
|
-
decrypted_data = Utils.a32_to_str(a32, data.size)
|
51
|
-
|
52
|
-
{data: decrypted_data, mac: mac}
|
4
|
+
def aes_ctr_cipher
|
5
|
+
OpenSSL::Cipher::AES.new(128, :CTR)
|
53
6
|
end
|
54
7
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
ab32 = Utils.str_to_a32 data
|
62
|
-
len = ab32.size - 3
|
63
|
-
enc = nil
|
64
|
-
last_i = 0
|
65
|
-
|
66
|
-
(0..len).step(4) do |i|
|
67
|
-
4.times { |x| mac[x] = mac[x] ^ (ab32[i+x] || 0) }
|
68
|
-
mac = Aes.encrypt key, mac
|
69
|
-
enc = Aes.encrypt key, ctr
|
70
|
-
4.times { |x| ab32[i+x] = (ab32[i+x] || 0) ^ (enc[x] || 0) }
|
71
|
-
ctr[3] += 1
|
72
|
-
ctr[2] += 1 if ctr[3].zero?
|
73
|
-
last_i = i + 4
|
74
|
-
end
|
75
|
-
|
76
|
-
i = last_i
|
77
|
-
|
78
|
-
if i < ab32.size
|
79
|
-
v = [0, 0, 0, 0]
|
80
|
-
(i..ab32.size - 1).step(1) { |j| v[j - i] = ab32[j] || 0 }
|
81
|
-
4.times { |x| mac[x] = mac[x] ^ v[x] }
|
82
|
-
mac = Aes.encrypt key, mac
|
83
|
-
enc = Aes.encrypt key, ctr
|
84
|
-
4.times { |x| v[x] = v[x] ^ enc[x] }
|
85
|
-
(i..ab32.size - 1).step(1) { |j| ab32[j] = v[j - i] || 0 }
|
86
|
-
end
|
87
|
-
|
88
|
-
decrypted_data = Utils.a32_to_str ab32, data.size
|
89
|
-
{data: decrypted_data, mac: mac}
|
8
|
+
def aes_ctr_decrypt(key, data, iv)
|
9
|
+
cipher = aes_ctr_cipher
|
10
|
+
cipher.decrypt
|
11
|
+
cipher.iv = iv
|
12
|
+
cipher.key = key
|
13
|
+
return cipher.update(data) + cipher.final
|
90
14
|
end
|
91
15
|
|
16
|
+
def aes_ctr_encrypt(key, data, iv)
|
17
|
+
cipher = aes_ctr_cipher
|
18
|
+
cipher.encrypt
|
19
|
+
cipher.iv = iv
|
20
|
+
cipher.key = key
|
21
|
+
return cipher.update(data) + cipher.final
|
22
|
+
end
|
92
23
|
end
|
93
24
|
end
|
94
25
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Crypto
|
3
|
+
module AesEcb
|
4
|
+
def aes_ecb_cipher
|
5
|
+
OpenSSL::Cipher::AES.new(128, :ECB)
|
6
|
+
end
|
7
|
+
|
8
|
+
def aes_ecb_encrypt(key, data)
|
9
|
+
cipher = aes_ecb_cipher
|
10
|
+
cipher.encrypt
|
11
|
+
cipher.padding = 0
|
12
|
+
cipher.key = key
|
13
|
+
return cipher.update(data) + cipher.final
|
14
|
+
end
|
15
|
+
|
16
|
+
def aes_ecb_decrypt(key, data)
|
17
|
+
cipher = aes_ecb_cipher
|
18
|
+
cipher.decrypt
|
19
|
+
cipher.padding = 0
|
20
|
+
cipher.key = key
|
21
|
+
return cipher.update(data) + cipher.final
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/rmega/crypto/rsa.rb
CHANGED
@@ -1,20 +1,29 @@
|
|
1
|
-
require 'execjs'
|
2
|
-
|
3
1
|
module Rmega
|
4
2
|
module Crypto
|
5
3
|
module Rsa
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
4
|
+
def powm(b, p, m)
|
5
|
+
if p == 1
|
6
|
+
b % m
|
7
|
+
elsif (p & 0x1) == 0
|
8
|
+
t = powm(b, p >> 1, m)
|
9
|
+
(t * t) % m
|
10
|
+
else
|
11
|
+
(b * powm(b, p-1, m)) % m
|
12
|
+
end
|
14
13
|
end
|
15
14
|
|
16
|
-
def
|
17
|
-
|
15
|
+
def rsa_decrypt(m, pqdu)
|
16
|
+
p, q, d, u = pqdu
|
17
|
+
if p && q && u
|
18
|
+
m1 = powm(m, d % (p - 1), p)
|
19
|
+
m2 = powm(m, d % (q - 1), q)
|
20
|
+
h = m2 - m1
|
21
|
+
h = h + q if h < 0
|
22
|
+
h = h * u % q
|
23
|
+
h * p + m1
|
24
|
+
else
|
25
|
+
pow_m(m, d, p * q)
|
26
|
+
end
|
18
27
|
end
|
19
28
|
end
|
20
29
|
end
|
data/lib/rmega/errors.rb
CHANGED
@@ -1,55 +1,7 @@
|
|
1
1
|
module Rmega
|
2
|
-
|
3
|
-
|
4
|
-
# Check out the error codes list at https://mega.co.nz/#doc (section 11)
|
5
|
-
CODES = {
|
6
|
-
-1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
|
7
|
-
-2 => 'You have passed invalid arguments to this command.',
|
8
|
-
-3 => 'A temporary congestion or server malfunction prevented your request from being processed. No data was altered. Retry. Retries must be spaced with exponential backoff.',
|
9
|
-
-4 => 'You have exceeded your command weight per time quota. Please wait a few seconds, then try again (this should never happen in sane real-life applications).',
|
10
|
-
-5 => 'The upload failed. Please restart it from scratch.',
|
11
|
-
-6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
|
12
|
-
-7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
|
13
|
-
-8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
|
14
|
-
-9 => 'Object (typically, node or user) not found',
|
15
|
-
-10 => 'Circular linkage attempted',
|
16
|
-
-11 => 'Access violation (e.g., trying to write to a read-only share)',
|
17
|
-
-12 => 'Trying to create an object that already exists',
|
18
|
-
-13 => 'Trying to access an incomplete resource',
|
19
|
-
-14 => 'A decryption operation failed (never returned by the API)',
|
20
|
-
-15 => 'Invalid or expired user session, please relogin',
|
21
|
-
-16 => 'User blocked',
|
22
|
-
-17 => 'Request over quota',
|
23
|
-
-18 => 'Resource temporarily not available, please try again later',
|
24
|
-
-19 => 'Too many connections on this resource',
|
25
|
-
-20 => 'Write failed',
|
26
|
-
-21 => 'Read failed',
|
27
|
-
-22 => 'Invalid application key; request not processed',
|
28
|
-
}.freeze
|
29
|
-
|
30
|
-
class ServerError < StandardError
|
31
|
-
attr_reader :code, :message
|
32
|
-
attr_accessor :temporary
|
33
|
-
|
34
|
-
def initialize(value, opts = {})
|
35
|
-
if msg = Errors::CODES[value.to_i]
|
36
|
-
@code = value.to_i
|
37
|
-
@message = msg || "Error code #{@code}"
|
38
|
-
else
|
39
|
-
@message = value
|
40
|
-
end
|
41
|
-
|
42
|
-
self.temporary = opts[:temporary] || false
|
43
|
-
|
44
|
-
super(@message)
|
45
|
-
end
|
46
|
-
|
47
|
-
def temporary?
|
48
|
-
self.temporary || [-3, -18, -6].include?(@code)
|
49
|
-
end
|
50
|
-
end
|
2
|
+
class ServerError < StandardError
|
51
3
|
end
|
52
4
|
|
53
|
-
|
54
|
-
|
5
|
+
class TemporaryServerError < StandardError
|
6
|
+
end
|
55
7
|
end
|
data/lib/rmega/loggable.rb
CHANGED
data/lib/rmega/net.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Net
|
3
|
+
include Loggable
|
4
|
+
include Options
|
5
|
+
|
6
|
+
def survive(retries = options.max_retries, &block)
|
7
|
+
yield
|
8
|
+
rescue ServerError
|
9
|
+
raise
|
10
|
+
rescue Exception => error
|
11
|
+
retries -= 1
|
12
|
+
raise(error) if retries < 0
|
13
|
+
logger.debug("[#{error.class}] #{error.message}. #{retries} attempt(s) left.")
|
14
|
+
sleep(options.retry_interval)
|
15
|
+
retry
|
16
|
+
end
|
17
|
+
|
18
|
+
def http_get_content(url)
|
19
|
+
uri = URI(url)
|
20
|
+
req = ::Net::HTTP::Get.new(uri.request_uri)
|
21
|
+
return send_http_request(uri, req).body
|
22
|
+
end
|
23
|
+
|
24
|
+
def http_post(url, data)
|
25
|
+
uri = URI(url)
|
26
|
+
req = ::Net::HTTP::Post.new(uri.request_uri)
|
27
|
+
req.body = data
|
28
|
+
logger.debug("REQ POST #{url} #{cut_string(data)}")
|
29
|
+
response = send_http_request(uri, req)
|
30
|
+
logger.debug("REP #{response.code} #{cut_string(response.body)}")
|
31
|
+
return response
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def send_http_request(uri, req)
|
37
|
+
http = ::Net::HTTP.new(uri.host, uri.port)
|
38
|
+
http.use_ssl = true if uri.scheme == 'https'
|
39
|
+
apply_http_options(http)
|
40
|
+
return http.request(req)
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_http_options(http)
|
44
|
+
http.proxy_from_env = false if options.http_proxy_address
|
45
|
+
|
46
|
+
options.marshal_dump.each do |name, value|
|
47
|
+
setter_method = name.to_s.split('http_')[1]
|
48
|
+
http.__send__("#{setter_method}=", value) if setter_method and value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def cut_string(string, max = 50)
|
53
|
+
string.size <= max ? string : string[0..max-1]+"..."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,17 +1,45 @@
|
|
1
|
-
require 'rmega/pool'
|
2
|
-
require 'rmega/utils'
|
3
|
-
|
4
1
|
module Rmega
|
5
2
|
module Nodes
|
6
3
|
module Downloadable
|
4
|
+
include Net
|
5
|
+
include Options
|
7
6
|
|
8
7
|
# Creates the local file allocating filesize-n bytes (of /dev/zero) for it.
|
9
8
|
# Opens the local file to start writing from the beginning of it.
|
10
9
|
def allocate(path)
|
11
|
-
|
12
|
-
|
10
|
+
unless allocated?(path)
|
11
|
+
`dd if=/dev/zero of="#{path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
|
12
|
+
raise "Unable to allocate space for file #{path}" if ::File.size(path) != filesize
|
13
|
+
end
|
14
|
+
|
15
|
+
@file = ::File.open(path, 'r+b')
|
16
|
+
@file.rewind
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_io_synchronize(&block)
|
20
|
+
@file_io_mutex ||= Mutex.new
|
21
|
+
@file_io_mutex.synchronize(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def allocated?(path)
|
25
|
+
::File.exists?(path) and ::File.size(path) == filesize
|
26
|
+
end
|
27
|
+
|
28
|
+
# Writes a buffer in the local file, starting from the start-n byte.
|
29
|
+
def write_chunk(start, buffer)
|
30
|
+
file_io_synchronize do
|
31
|
+
@file.seek(start)
|
32
|
+
@file.write(buffer)
|
33
|
+
end
|
34
|
+
end
|
13
35
|
|
14
|
-
|
36
|
+
def read_chunk(start, size)
|
37
|
+
file_io_synchronize do
|
38
|
+
@file.seek(start)
|
39
|
+
data = @file.read(size)
|
40
|
+
@file.seek(start)
|
41
|
+
return (data == "\x0"*size) ? nil : data
|
42
|
+
end
|
15
43
|
end
|
16
44
|
|
17
45
|
# Downloads a part of the remote file, starting from the start-n byte
|
@@ -19,52 +47,67 @@ module Rmega
|
|
19
47
|
def download_chunk(start, size)
|
20
48
|
stop = start + size - 1
|
21
49
|
url = "#{storage_url}/#{start}-#{stop}"
|
22
|
-
|
50
|
+
|
51
|
+
survive do
|
52
|
+
data = http_get_content(url)
|
53
|
+
raise("Unexpected data length") if data.size != size
|
54
|
+
return data
|
55
|
+
end
|
23
56
|
end
|
24
57
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
file.write(buffer)
|
58
|
+
def decrypt_chunk(start, data)
|
59
|
+
iv = @node_key.ctr_nonce + [start/0x1000000000, start/0x10].pack('l>*')
|
60
|
+
return aes_ctr_decrypt(@node_key.aes_key, data, iv)
|
29
61
|
end
|
30
62
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
decrypt_key = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
|
35
|
-
Crypto::AesCtr.decrypt(decrypt_key, nonce, encrypted_buffer)[:data]
|
63
|
+
def calculate_chunck_mac(data)
|
64
|
+
mac_iv = @node_key.ctr_nonce * 2
|
65
|
+
return aes_cbc_mac(@node_key.aes_key, data, mac_iv)
|
36
66
|
end
|
37
67
|
|
38
68
|
def download(path)
|
39
69
|
path = ::File.expand_path(path)
|
40
70
|
path = Dir.exists?(path) ? ::File.join(path, name) : path
|
41
71
|
|
42
|
-
|
43
|
-
|
72
|
+
progress = Progress.new(filesize, caption: 'Download')
|
44
73
|
pool = Pool.new
|
45
|
-
write_mutex = Mutex.new
|
46
|
-
file = allocate(path)
|
47
74
|
|
48
|
-
|
75
|
+
@resumed_download = allocated?(path)
|
76
|
+
allocate(path)
|
77
|
+
@node_key = NodeKey.load(decrypted_file_key)
|
49
78
|
|
50
|
-
|
51
|
-
pool.defer do
|
52
|
-
encrypted_buffer = download_chunk(start, size)
|
79
|
+
chunk_macs = {}
|
53
80
|
|
54
|
-
|
55
|
-
|
81
|
+
each_chunk do |start, size|
|
82
|
+
pool.process do
|
83
|
+
data = @resumed_download ? read_chunk(start, size) : nil
|
84
|
+
|
85
|
+
if data
|
86
|
+
chunk_macs[start] = calculate_chunck_mac(data) if options.file_integrity_check
|
87
|
+
progress.increment(size, real: false)
|
88
|
+
else
|
89
|
+
data = decrypt_chunk(start, download_chunk(start, size))
|
90
|
+
chunk_macs[start] = calculate_chunck_mac(data) if options.file_integrity_check
|
91
|
+
write_chunk(start, data)
|
56
92
|
progress.increment(size)
|
57
|
-
write_chunk(file, start, clean_buffer)
|
58
93
|
end
|
59
94
|
end
|
60
95
|
end
|
61
96
|
|
62
97
|
# waits for the last running threads to finish
|
63
|
-
pool.
|
98
|
+
pool.shutdown
|
99
|
+
|
100
|
+
if options.file_integrity_check
|
101
|
+
file_mac = aes_cbc_mac(@node_key.aes_key, chunk_macs.sort.map(&:last).join, "\x0"*16)
|
102
|
+
|
103
|
+
if Utils.compact_to_8_bytes(file_mac) != @node_key.meta_mac
|
104
|
+
raise("Checksum failed. File corrupted?")
|
105
|
+
end
|
106
|
+
end
|
64
107
|
|
65
|
-
|
108
|
+
return nil
|
66
109
|
ensure
|
67
|
-
file.close rescue nil
|
110
|
+
@file.close rescue nil
|
68
111
|
end
|
69
112
|
end
|
70
113
|
end
|