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,19 +1,23 @@
1
- require 'rmega/utils'
2
- require 'rmega/nodes/uploadable'
3
- require 'rmega/crypto/crypto'
4
-
5
1
  module Rmega
6
2
  module Nodes
7
3
  module Expandable
8
4
  include Uploadable
9
5
 
10
6
  def create_folder(name)
11
- key = Crypto.random_key
12
- encrypted_attributes = Utils.a32_to_base64 Crypto.encrypt_attributes(key[0..3], {n: name.strip})
13
- encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
14
- n = [{h: 'xxxxxxxx', t: 1, a: encrypted_attributes, k: encrypted_key}]
15
- data = session.request a: 'p', t: handle, n: n
16
- Folder.new(session, data['f'][0])
7
+ node_key = NodeKey.random
8
+
9
+ # encrypt attributes
10
+ attributes_str = "MEGA"
11
+ attributes_str << {n: name.strip}.to_json
12
+ attributes_str << ("\x00" * (16 - (attributes_str.size % 16)))
13
+ encrypted_attributes = aes_cbc_encrypt(node_key.aes_key, attributes_str)
14
+
15
+ # Encrypt node key
16
+ encrypted_key = aes_ecb_encrypt(session.master_key, node_key.aes_key)
17
+
18
+ n = [{h: 'xxxxxxxx', t: 1, a: Utils.base64urlencode(encrypted_attributes), k: Utils.base64urlencode(encrypted_key)}]
19
+ data = session.request(a: 'p', t: handle, n: n)
20
+ return Folder.new(session, data['f'][0])
17
21
  end
18
22
 
19
23
  def upload_url(filesize)
@@ -1,3 +1,9 @@
1
+ require 'rmega/nodes/uploadable'
2
+ require 'rmega/nodes/expandable'
3
+ require 'rmega/nodes/downloadable'
4
+ require 'rmega/nodes/deletable'
5
+ require 'rmega/nodes/traversable'
6
+ require 'rmega/nodes/node_key'
1
7
  require 'rmega/nodes/node'
2
8
  require 'rmega/nodes/file'
3
9
  require 'rmega/nodes/folder'
@@ -10,31 +16,38 @@ module Rmega
10
16
  module Factory
11
17
  extend self
12
18
 
19
+ URL_REGEXP = /mega\..+\/\#([A-Z0-9\_\-\!\=]+)/i
20
+
21
+ FOLDER_URL_REGEXP = /mega\..+\/\#\F([A-Z0-9\_\-\!\=]+)/i
22
+
23
+ def url?(string)
24
+ string.to_s =~ URL_REGEXP
25
+ end
26
+
13
27
  def build(session, data)
14
- type_name = type(data['t'])
15
- node_class = Nodes.const_get("#{type_name.to_s.capitalize}")
16
- node_class.new(session, data)
28
+ type = Node::TYPES[data['t']].to_s
29
+ return Nodes.const_get(type.capitalize).new(session, data)
17
30
  end
18
31
 
19
- # TODO: support other node types than File
20
- def build_from_url(session, url)
32
+ def build_from_url(url, session = Session.new)
21
33
  public_handle, key = url.strip.split('!')[1, 2]
22
- data = session.request(a: 'g', g: 1, p: public_handle)
23
34
 
24
- Nodes::File.new(session, data).tap { |n| n.public_url = url }
25
- end
35
+ raise "Invalid url or missing file key" unless key
26
36
 
27
- def mega_url?(url)
28
- !!(url.to_s =~ /^https:\/\/mega\.co\.nz\/#!.*$/i)
29
- end
37
+ node = if url =~ FOLDER_URL_REGEXP
38
+ nodes_data = session.request({a: 'f', c: 1, r: 1}, {n: public_handle})
39
+ session.master_key = Utils.base64urldecode(key)
40
+ session.storage.nodes = nodes_data['f'].map { |data| Nodes::Factory.build(session, data) }
41
+ session.storage.nodes[0]
42
+ else
43
+ data = session.request(a: 'g', g: 1, p: public_handle)
44
+ Nodes::File.new(session, data)
45
+ end
30
46
 
31
- def type(number)
32
- founded_type = types.find { |k, v| number == v }
33
- founded_type.first if founded_type
34
- end
47
+ node.instance_variable_set('@public_handle', public_handle)
48
+ node.instance_variable_set('@public_url', url)
35
49
 
36
- def types
37
- {file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
50
+ return node
38
51
  end
39
52
  end
40
53
  end
@@ -1,7 +1,3 @@
1
- require 'rmega/nodes/node'
2
- require 'rmega/nodes/deletable'
3
- require 'rmega/nodes/downloadable'
4
-
5
1
  module Rmega
6
2
  module Nodes
7
3
  class File < Node
@@ -1,10 +1,3 @@
1
- require 'rmega/crypto/crypto'
2
- require 'rmega/utils'
3
- require 'rmega/nodes/node'
4
- require 'rmega/nodes/expandable'
5
- require 'rmega/nodes/traversable'
6
- require 'rmega/nodes/deletable'
7
-
8
1
  module Rmega
9
2
  module Nodes
10
3
  class Folder < Node
@@ -13,14 +6,11 @@ module Rmega
13
6
  include Deletable
14
7
 
15
8
  def download(path)
9
+ path = ::File.join(path, self.name)
10
+ FileUtils.mkdir_p(path)
11
+
16
12
  children.each do |node|
17
- if node.type == :file
18
- node.download path
19
- elsif node.type == :folder
20
- subfolder = ::File.expand_path ::File.join(path, node.name)
21
- Dir.mkdir(subfolder) unless Dir.exists?(subfolder)
22
- node.download subfolder
23
- end
13
+ node.download(path)
24
14
  end
25
15
 
26
16
  nil
@@ -1,5 +1,3 @@
1
- require 'rmega/nodes/node'
2
-
3
1
  module Rmega
4
2
  module Nodes
5
3
  class Inbox < Node
@@ -1,17 +1,16 @@
1
- require 'rmega/loggable'
2
- require 'rmega/utils'
3
- require 'rmega/crypto/crypto'
4
- require 'rmega/nodes/traversable'
5
-
6
1
  module Rmega
7
2
  module Nodes
8
3
  class Node
4
+ include NotInspectable
9
5
  include Loggable
10
6
  include Traversable
7
+ include Crypto
11
8
 
12
9
  attr_reader :data, :session
13
10
 
14
- delegate :storage, :request, :shared_keys, :rsa_privk, :to => :session
11
+ delegate :request, :shared_keys, :rsa_privk, :master_key, :storage, :to => :session
12
+
13
+ TYPES = {0 => :file, 1 => :folder, 2 => :root, 3 => :inbox, 4 => :trash}
15
14
 
16
15
  def initialize(session, data)
17
16
  @session = session
@@ -19,14 +18,7 @@ module Rmega
19
18
  end
20
19
 
21
20
  def public_url
22
- @public_url ||= begin
23
- b64_dec_key = Utils.a32_to_base64(decrypted_file_key[0..7])
24
- "https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
25
- end
26
- end
27
-
28
- def public_url=(url)
29
- @public_url = url
21
+ @public_url ||= "https://mega.co.nz/#!#{public_handle}!#{Utils.base64urlencode(decrypted_file_key)}"
30
22
  end
31
23
 
32
24
  def public_handle
@@ -51,13 +43,14 @@ module Rmega
51
43
  pairs = data['k'].split('/')
52
44
  pairs.inject({}) do |hash, pair|
53
45
  h, k = pair.split(':')
54
- hash[h] = k
46
+ hash[h] = Utils.base64urldecode(k)
55
47
  hash
56
48
  end
57
49
  end
58
50
 
59
51
  def file_key
60
- file_keys.values.first
52
+ k = file_keys.values.first
53
+ return k ? k : nil
61
54
  end
62
55
 
63
56
  def shared_root?
@@ -73,37 +66,67 @@ module Rmega
73
66
  return unless sk
74
67
 
75
68
  shared_key = if sk.size > 22
76
- sk = Rmega::Utils.mpi2b(Rmega::Utils.base64urldecode(sk))
77
- dec_sk = Rmega::Crypto::Rsa.decrypt(sk, rsa_privk)
78
- Utils.str_to_a32(Rmega::Utils.b2s(dec_sk)[0..15])
69
+ # Decrypt sk
70
+ sk = Utils.base64_mpi_to_bn(sk)
71
+ sk = rsa_decrypt(sk, rsa_privk)
72
+ sk = sk.to_s(16)
73
+ sk = '0' + sk if sk.size % 2 > 0
74
+ Utils.hexstr_to_bstr(sk)[0..15]
79
75
  else
80
- Crypto.decrypt_key session.master_key, Utils.base64_to_a32(data['sk'])
76
+ aes_ecb_decrypt(master_key, Utils.base64urldecode(sk))
81
77
  end
82
78
 
83
79
  shared_keys[handle] = shared_key
84
80
  [handle, shared_key]
85
81
  end
86
82
 
83
+ def self.each_chunk(size, &block)
84
+ start, p = 0, 0
85
+
86
+ return if size <= 0
87
+
88
+ loop do
89
+ offset = p < 8 ? (131072 * (p += 1)) : 1048576
90
+ next_start = offset + start
91
+
92
+ if next_start >= size
93
+ yield(start, size - start)
94
+ break
95
+ else
96
+ yield(start, offset)
97
+ start = next_start
98
+ end
99
+ end
100
+ end
101
+
102
+ def each_chunk(&block)
103
+ self.class.each_chunk(filesize, &block)
104
+ end
105
+
87
106
  def decrypted_file_key
88
107
  h, shared_key = *process_shared_key
89
108
 
90
109
  if shared_key
91
- Crypto.decrypt_key(shared_key, Utils.base64_to_a32(file_keys[h]))
110
+ aes_ecb_decrypt(shared_key, file_keys[h])
92
111
  elsif file_key
93
- Crypto.decrypt_key(session.master_key, Utils.base64_to_a32(file_key))
112
+ aes_ecb_decrypt(master_key, file_key)
94
113
  else
95
- Utils.base64_to_a32(public_url.split('!').last)
114
+ Utils.base64urldecode(public_url.split('!').last)
96
115
  end
97
116
  end
98
117
 
99
118
  def attributes
100
119
  encrypted = data['a'] || data['at']
101
120
  return if !encrypted or encrypted.empty?
102
- Crypto.decrypt_attributes(decrypted_file_key, encrypted)
121
+ node_key = NodeKey.load(decrypted_file_key)
122
+ encrypted = Utils.base64urldecode(encrypted)
123
+ encrypted.strip! if encrypted.size % 16 != 0 # Fix possible errors
124
+ json = aes_cbc_decrypt(node_key.aes_key, encrypted)
125
+ JSON.parse json.gsub(/^MEGA/, '').rstrip
103
126
  end
104
127
 
105
128
  def type
106
- Factory.type(data['t'])
129
+ TYPES[data['t']]
107
130
  end
108
131
  end
109
132
  end
@@ -0,0 +1,44 @@
1
+ module Rmega
2
+ module Nodes
3
+
4
+ # The key associated to a node. It can be 128 or 256 bits long,
5
+ # when is 256 bits long is composed by:
6
+ # * A 128 bit AES-128 key
7
+ # * The upper 64 bit of the counter start value (the lower 64 bit
8
+ # are starting at 0 and incrementing by 1 for each AES block of 16
9
+ # bytes)
10
+ # * A 64 bit meta-MAC of all chunk MACs
11
+ class NodeKey
12
+ attr_reader :aes_key, :ctr_nonce, :meta_mac
13
+ attr_accessor :meta_mac
14
+
15
+ def initialize(string)
16
+ @aes_key = string[0..15]
17
+ @ctr_nonce = string[16..23]
18
+ @meta_mac = string[24..31]
19
+ end
20
+
21
+ def generate
22
+ self.class.compact("#{@aes_key}#{@ctr_nonce}#{@meta_mac}") + @ctr_nonce + @meta_mac
23
+ end
24
+
25
+ # note: folder key is 16 bytes long while file key is 32
26
+ def self.load(string)
27
+ new("#{compact(string)}#{string[16..-1]}")
28
+ end
29
+
30
+ def self.compact(string)
31
+ if string.size > 16
32
+ bytes = string.bytes.to_a
33
+ return 16.times.inject([]) { |ary, i| ary[i] = bytes[i] ^ bytes[i+16]; ary }.map(&:chr).join
34
+ else
35
+ return string
36
+ end
37
+ end
38
+
39
+ def self.random
40
+ new(OpenSSL::Random.random_bytes(16 + 8 + 0))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,7 +1,3 @@
1
- require 'rmega/nodes/node'
2
- require 'rmega/nodes/expandable'
3
- require 'rmega/nodes/traversable'
4
-
5
1
  module Rmega
6
2
  module Nodes
7
3
  class Root < Node
@@ -1,6 +1,3 @@
1
- require 'rmega/nodes/node'
2
- require 'rmega/nodes/traversable'
3
-
4
1
  module Rmega
5
2
  module Nodes
6
3
  class Trash < Node
@@ -1,16 +1,18 @@
1
- require 'rmega/utils'
2
- require 'rmega/pool'
3
- require 'rmega/progress'
4
-
5
1
  module Rmega
6
2
  module Nodes
7
3
  module Uploadable
4
+ include Net
5
+
8
6
  def upload_chunk(base_url, start, buffer)
9
7
  size = buffer.length
10
8
  stop = start + size - 1
11
9
  url = "#{base_url}/#{start}-#{stop}"
12
10
 
13
- HTTPClient.new.post(url, buffer).body
11
+ survive do
12
+ response = http_post(url, buffer)
13
+ raise("Upload failed") if response.code.to_i != 200
14
+ return response.body
15
+ end
14
16
  end
15
17
 
16
18
  def read_chunk(file, start, size)
@@ -18,65 +20,72 @@ module Rmega
18
20
  file.read(size)
19
21
  end
20
22
 
21
- def encrypt_chunck(rnd_key, file_mac, start, clean_buffer)
22
- nonce = [rnd_key[4], rnd_key[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
23
-
24
- encrypted = Crypto::AesCtr.encrypt(rnd_key[0..3], nonce, clean_buffer)
25
- chunk_mac, data = encrypted[:mac], encrypted[:data]
26
-
27
- file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
28
- file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
23
+ def encrypt_chunck(start, clean_buffer, aes_key, nonce)
24
+ iv = nonce + [start/0x1000000000, start/0x10].pack('l>*')
25
+ enc_data = aes_ctr_encrypt(aes_key, clean_buffer, iv)
29
26
 
30
- file_mac = Crypto::Aes.encrypt(rnd_key[0..3], file_mac)
27
+ # calculate mac
28
+ mac_iv = nonce * 2
29
+ mac = aes_cbc_mac(aes_key, clean_buffer, mac_iv)
31
30
 
32
- data
31
+ return [enc_data, mac]
33
32
  end
34
33
 
35
34
  def upload(path)
36
35
  path = ::File.expand_path(path)
37
36
  filesize = ::File.size(path)
37
+
38
+ raise "Empty file - #{path}" if filesize == 0
39
+
38
40
  file = ::File.open(path, 'rb')
39
41
 
40
- ul_key = Crypto.random_key
41
- file_mac = [0, 0, 0, 0]
42
+ rnd_node_key = NodeKey.random
42
43
  file_handle = nil
43
44
  base_url = upload_url(filesize)
44
45
 
45
46
  pool = Pool.new
46
47
  read_mutex = Mutex.new
47
48
 
48
- progress = Progress.new(total: filesize, caption: 'Upload')
49
+ progress = Progress.new(filesize, caption: 'Upload')
50
+
51
+ chunk_macs = {}
49
52
 
50
- Utils.chunks(filesize).each do |start, size|
51
- pool.defer do
52
- encrypted_buffer = nil
53
+ self.class.each_chunk(filesize) do |start, size|
54
+ pool.process do
55
+ clean_buffer = nil
53
56
 
54
57
  read_mutex.synchronize do
55
58
  clean_buffer = read_chunk(file, start, size)
56
- encrypted_buffer = encrypt_chunck(ul_key, file_mac, start, clean_buffer)
57
59
  end
58
60
 
61
+ encrypted_buffer, chunk_mac = *encrypt_chunck(start, clean_buffer, rnd_node_key.aes_key, rnd_node_key.ctr_nonce)
59
62
  file_handle = upload_chunk(base_url, start, encrypted_buffer)
63
+ chunk_macs[start] = chunk_mac
64
+
60
65
  progress.increment(size)
61
66
  end
62
67
  end
63
68
 
64
- pool.wait_done
65
-
66
- attribs = {n: ::File.basename(path)}
67
- encrypt_attribs = Utils.a32_to_base64(Crypto.encrypt_attributes(ul_key[0..3], attribs))
69
+ pool.shutdown
68
70
 
69
- meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
71
+ # encrypt attributes
72
+ attributes_str = "MEGA"
73
+ attributes_str << {n: ::File.basename(path)}.to_json
74
+ attributes_str << ("\x00" * (16 - (attributes_str.size % 16)))
75
+ encrypted_attributes = aes_cbc_encrypt(rnd_node_key.aes_key, attributes_str)
70
76
 
71
- key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
72
- ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
77
+ # Calculate meta_mac
78
+ file_mac = aes_cbc_mac(rnd_node_key.aes_key, chunk_macs.sort.map(&:last).join, "\x0"*16)
79
+ rnd_node_key.meta_mac = Utils.compact_to_8_bytes(file_mac)
80
+ encrypted_key = aes_ecb_encrypt(session.master_key, rnd_node_key.generate)
73
81
 
74
- encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
75
- request(a: 'p', t: handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}])
82
+ resp = request(a: 'p', t: handle, n: [
83
+ {h: file_handle, t: 0, a: Utils.base64urlencode(encrypted_attributes), k: Utils.base64urlencode(encrypted_key)}
84
+ ])
76
85
 
77
- attribs[:n]
86
+ return Nodes::Factory.build(session, resp['f'][0])
78
87
  ensure
79
- file.close
88
+ file.close if file
80
89
  end
81
90
  end
82
91
  end