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
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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)
|
data/lib/rmega/nodes/factory.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
25
|
-
end
|
35
|
+
raise "Invalid url or missing file key" unless key
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
37
|
-
{file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
|
50
|
+
return node
|
38
51
|
end
|
39
52
|
end
|
40
53
|
end
|
data/lib/rmega/nodes/file.rb
CHANGED
data/lib/rmega/nodes/folder.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/rmega/nodes/inbox.rb
CHANGED
data/lib/rmega/nodes/node.rb
CHANGED
@@ -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 :
|
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 ||=
|
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
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
-
|
110
|
+
aes_ecb_decrypt(shared_key, file_keys[h])
|
92
111
|
elsif file_key
|
93
|
-
|
112
|
+
aes_ecb_decrypt(master_key, file_key)
|
94
113
|
else
|
95
|
-
Utils.
|
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
|
-
|
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
|
-
|
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
|
data/lib/rmega/nodes/root.rb
CHANGED
data/lib/rmega/nodes/trash.rb
CHANGED
@@ -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
|
-
|
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(
|
22
|
-
|
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
|
-
|
27
|
+
# calculate mac
|
28
|
+
mac_iv = nonce * 2
|
29
|
+
mac = aes_cbc_mac(aes_key, clean_buffer, mac_iv)
|
31
30
|
|
32
|
-
|
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
|
-
|
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(
|
49
|
+
progress = Progress.new(filesize, caption: 'Upload')
|
50
|
+
|
51
|
+
chunk_macs = {}
|
49
52
|
|
50
|
-
|
51
|
-
pool.
|
52
|
-
|
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.
|
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
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
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
|