rmega 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +1 -1
  5. data/TODO.md +3 -5
  6. data/bin/rmega-dl +47 -0
  7. data/bin/rmega-up +31 -0
  8. data/lib/rmega.rb +35 -3
  9. data/lib/rmega/api_response.rb +80 -0
  10. data/lib/rmega/cli.rb +121 -0
  11. data/lib/rmega/crypto.rb +20 -0
  12. data/lib/rmega/crypto/aes_cbc.rb +46 -0
  13. data/lib/rmega/crypto/aes_ctr.rb +15 -84
  14. data/lib/rmega/crypto/aes_ecb.rb +25 -0
  15. data/lib/rmega/crypto/rsa.rb +21 -12
  16. data/lib/rmega/errors.rb +3 -51
  17. data/lib/rmega/loggable.rb +0 -3
  18. data/lib/rmega/net.rb +56 -0
  19. data/lib/rmega/nodes/deletable.rb +0 -3
  20. data/lib/rmega/nodes/downloadable.rb +73 -30
  21. data/lib/rmega/nodes/expandable.rb +14 -10
  22. data/lib/rmega/nodes/factory.rb +30 -17
  23. data/lib/rmega/nodes/file.rb +0 -4
  24. data/lib/rmega/nodes/folder.rb +4 -14
  25. data/lib/rmega/nodes/inbox.rb +0 -2
  26. data/lib/rmega/nodes/node.rb +48 -25
  27. data/lib/rmega/nodes/node_key.rb +44 -0
  28. data/lib/rmega/nodes/root.rb +0 -4
  29. data/lib/rmega/nodes/trash.rb +0 -3
  30. data/lib/rmega/nodes/uploadable.rb +42 -33
  31. data/lib/rmega/not_inspectable.rb +10 -0
  32. data/lib/rmega/options.rb +22 -5
  33. data/lib/rmega/pool.rb +18 -7
  34. data/lib/rmega/progress.rb +53 -13
  35. data/lib/rmega/session.rb +125 -52
  36. data/lib/rmega/storage.rb +25 -21
  37. data/lib/rmega/utils.rb +23 -183
  38. data/lib/rmega/version.rb +2 -1
  39. data/rmega.gemspec +3 -5
  40. data/spec/integration/file_download_spec.rb +14 -32
  41. data/spec/integration/file_integrity_spec.rb +41 -0
  42. data/spec/integration/file_upload_spec.rb +11 -57
  43. data/spec/integration/folder_download_spec.rb +17 -0
  44. data/spec/integration/folder_operations_spec.rb +30 -30
  45. data/spec/integration/login_spec.rb +3 -3
  46. data/spec/integration/resume_download_spec.rb +53 -0
  47. data/spec/integration_spec_helper.rb +9 -4
  48. data/spec/rmega/lib/cli_spec.rb +12 -0
  49. data/spec/rmega/lib/session_spec.rb +31 -0
  50. data/spec/rmega/lib/storage_spec.rb +27 -0
  51. data/spec/rmega/lib/utils_spec.rb +16 -78
  52. data/spec/spec_helper.rb +1 -4
  53. metadata +30 -40
  54. data/lib/rmega/crypto/aes.rb +0 -35
  55. data/lib/rmega/crypto/crypto.rb +0 -107
  56. data/lib/rmega/crypto/rsa_mega.js +0 -455
  57. data/spec/rmega/lib/crypto/aes_spec.rb +0 -12
  58. data/spec/rmega/lib/crypto/crypto_spec.rb +0 -27
@@ -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
- extend self
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 encrypt(key, nonce, data)
56
- raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
57
- raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
58
-
59
- ctr = nonce.dup
60
- mac = [ctr[0], ctr[1], ctr[0], ctr[1]]
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
@@ -1,20 +1,29 @@
1
- require 'execjs'
2
-
3
1
  module Rmega
4
2
  module Crypto
5
3
  module Rsa
6
- extend self
7
-
8
- def script_path
9
- File.join File.dirname(__FILE__), 'rsa_mega.js'
10
- end
11
-
12
- def context
13
- @context ||= ExecJS.compile File.read(script_path)
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 decrypt(t, privk)
17
- context.call "RSAdecrypt", t, privk[2], privk[0], privk[1], privk[3]
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
- module Errors
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
- # Backward compatibility
54
- RequestError = Errors::ServerError
5
+ class TemporaryServerError < StandardError
6
+ end
55
7
  end
@@ -1,6 +1,3 @@
1
- require 'logger'
2
- require 'active_support/concern'
3
-
4
1
  module Rmega
5
2
  def self.logger
6
3
  @logger ||= begin
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,6 +1,3 @@
1
- require 'rmega/utils'
2
- require 'rmega/crypto/crypto'
3
-
4
1
  module Rmega
5
2
  module Nodes
6
3
  module Deletable
@@ -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
- `dd if=/dev/zero of="#{path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
12
- raise "Unable to create file #{path}" if ::File.size(path) != filesize
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
- ::File.open(path, 'r+b').tap { |f| f.rewind }
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
- HTTPClient.new.get_content(url)
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
- # Writes a buffer in the local file, starting from the start-n byte.
26
- def write_chunk(file, start, buffer)
27
- file.seek(start)
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 decrypt_chunk(start, encrypted_buffer)
32
- k = decrypted_file_key
33
- nonce = [k[4], k[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
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
- logger.info "Download #{name} (#{filesize} bytes) => #{path}"
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
- progress = Progress.new(total: filesize, caption: 'Download')
75
+ @resumed_download = allocated?(path)
76
+ allocate(path)
77
+ @node_key = NodeKey.load(decrypted_file_key)
49
78
 
50
- Utils.chunks(filesize).each do |start, size|
51
- pool.defer do
52
- encrypted_buffer = download_chunk(start, size)
79
+ chunk_macs = {}
53
80
 
54
- write_mutex.synchronize do
55
- clean_buffer = decrypt_chunk(start, encrypted_buffer)
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.wait_done
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
- file.flush
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