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,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