rmega 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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