rmega 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +82 -0
- data/Rakefile +2 -0
- data/lib/rmega.rb +50 -0
- data/lib/rmega/crypto/aes.rb +33 -0
- data/lib/rmega/crypto/aes_ctr.rb +89 -0
- data/lib/rmega/crypto/crypto.rb +94 -0
- data/lib/rmega/crypto/rsa.rb +19 -0
- data/lib/rmega/crypto/rsa_mega.js +455 -0
- data/lib/rmega/node.rb +128 -0
- data/lib/rmega/session.rb +78 -0
- data/lib/rmega/storage.rb +146 -0
- data/lib/rmega/utils.rb +197 -0
- data/lib/rmega/version.rb +3 -0
- data/rmega.gemspec +23 -0
- data/spec/rmega/lib/crypto/aes_spec.rb +12 -0
- data/spec/rmega/lib/crypto/crypto_spec.rb +27 -0
- data/spec/rmega/lib/utils_spec.rb +66 -0
- data/spec/spec_helper.rb +12 -0
- metadata +150 -0
data/lib/rmega/node.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module Rmega
|
2
|
+
class Node
|
3
|
+
attr_reader :data, :session
|
4
|
+
|
5
|
+
def initialize session, data
|
6
|
+
@session = session
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.initialize_by_public_url session, public_url
|
11
|
+
public_handle, key = public_url.split('!')[1, 2]
|
12
|
+
data = session.request a: 'g', g: 1, p: public_handle
|
13
|
+
node = new session, data
|
14
|
+
node.instance_variable_set '@public_url', public_url
|
15
|
+
node
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.types
|
19
|
+
{file: 0, dir: 1, root: 2, inbox: 3, trash: 4}
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Member actions
|
24
|
+
|
25
|
+
def public_url
|
26
|
+
return @public_url if @public_url
|
27
|
+
return nil if type != :file
|
28
|
+
b64_dec_key = Utils.a32_to_base64 decrypted_file_key[0..7]
|
29
|
+
"https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def trash
|
33
|
+
trash_node_public_handle = session.storage.trash_node.public_handle
|
34
|
+
session.request a: 'm', n: handle, t: trash_node_public_handle
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Other methods
|
39
|
+
|
40
|
+
def public_handle
|
41
|
+
@public_handle ||= session.request(a: 'l', n: handle)
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle
|
45
|
+
data['h']
|
46
|
+
end
|
47
|
+
|
48
|
+
def filesize
|
49
|
+
data['s']
|
50
|
+
end
|
51
|
+
|
52
|
+
def owner_key
|
53
|
+
data['k'].split(':').first
|
54
|
+
end
|
55
|
+
|
56
|
+
def name
|
57
|
+
return attributes['n'] if attributes
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_key
|
61
|
+
data['k'].split(':').last
|
62
|
+
end
|
63
|
+
|
64
|
+
def decrypted_file_key
|
65
|
+
if data['k']
|
66
|
+
Crypto.decrypt_key session.master_key, Utils.base64_to_a32(file_key)
|
67
|
+
else
|
68
|
+
Utils.base64_to_a32 public_url.split('!').last
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def can_decrypt_attributes?
|
73
|
+
!data['u'] or data['u'] == owner_key
|
74
|
+
end
|
75
|
+
|
76
|
+
def attributes
|
77
|
+
@attributes ||= begin
|
78
|
+
return nil unless can_decrypt_attributes?
|
79
|
+
Crypto.decrypt_attributes decrypted_file_key, (data['a'] || data['at'])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def type
|
84
|
+
founded_type = self.class.types.find { |k, v| data['t'] == v }
|
85
|
+
founded_type.first if founded_type
|
86
|
+
end
|
87
|
+
|
88
|
+
def delete
|
89
|
+
session.request a: 'd', n: handle
|
90
|
+
end
|
91
|
+
|
92
|
+
def storage_url
|
93
|
+
@storage_url ||= data['g'] || session.request(a: 'g', g: 1, n: handle)['g']
|
94
|
+
end
|
95
|
+
|
96
|
+
def chunks
|
97
|
+
Storage.chunks filesize
|
98
|
+
end
|
99
|
+
|
100
|
+
def download path
|
101
|
+
path = File.expand_path path
|
102
|
+
path = Dir.exists?(path) ? File.join(path, name) : path
|
103
|
+
|
104
|
+
Utils.show_progress :download, filesize
|
105
|
+
|
106
|
+
k = decrypted_file_key
|
107
|
+
k = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
|
108
|
+
nonce = decrypted_file_key[4..5]
|
109
|
+
|
110
|
+
file = File.open path, 'wb'
|
111
|
+
connection = HTTPClient.new.get_async storage_url
|
112
|
+
message = connection.pop
|
113
|
+
|
114
|
+
chunks.each do |chunk_start, chunk_size|
|
115
|
+
buffer = message.content.read chunk_size
|
116
|
+
# TODO: should be (chunk_start/0x1000000000) >>> 0, (chunk_start/0x10) >>> 0
|
117
|
+
nonce = [nonce[0], nonce[1], (chunk_start/0x1000000000) >> 0, (chunk_start/0x10) >> 0]
|
118
|
+
decryption_result = Crypto::AesCtr.decrypt(k, nonce, buffer)
|
119
|
+
file.write(decryption_result[:data])
|
120
|
+
Utils.show_progress :download, filesize, chunk_size
|
121
|
+
end
|
122
|
+
|
123
|
+
nil
|
124
|
+
ensure
|
125
|
+
file.close if file
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Rmega
|
2
|
+
class Session
|
3
|
+
attr_accessor :email, :request_id, :sid, :master_key
|
4
|
+
|
5
|
+
def initialize email, password_str
|
6
|
+
self.email = email
|
7
|
+
self.request_id = random_request_id
|
8
|
+
|
9
|
+
login password_str
|
10
|
+
end
|
11
|
+
|
12
|
+
def debug message
|
13
|
+
Rmega.logger.debug message
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# Delegate to Rmega.options
|
18
|
+
|
19
|
+
def options
|
20
|
+
Rmega.options
|
21
|
+
end
|
22
|
+
|
23
|
+
def api_request_timeout
|
24
|
+
options.api_request_timeout
|
25
|
+
end
|
26
|
+
|
27
|
+
def api_url
|
28
|
+
options.api_url
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Cache the Storage class
|
33
|
+
|
34
|
+
def storage
|
35
|
+
@storage ||= Storage.new self
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Login-related methods
|
40
|
+
|
41
|
+
def login password_str
|
42
|
+
uh = Crypto.stringhash Crypto.prepare_key_pw(password_str), email
|
43
|
+
resp = request a: 'us', user: email, uh: uh
|
44
|
+
|
45
|
+
# Decrypt the master key
|
46
|
+
encrypted_key = Crypto.prepare_key_pw password_str
|
47
|
+
self.master_key = Crypto.decrypt_key encrypted_key, Utils.base64_to_a32(resp['k'])
|
48
|
+
|
49
|
+
# Generate the session id
|
50
|
+
self.sid = Crypto.decrypt_sid master_key, resp['csid'], resp['privk']
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Api requests methods
|
55
|
+
|
56
|
+
def random_request_id
|
57
|
+
rand(1E7..1E9).to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
def error_response? response
|
61
|
+
response = response.first if response.respond_to? :first
|
62
|
+
!!Integer(response) rescue false
|
63
|
+
end
|
64
|
+
|
65
|
+
def request body
|
66
|
+
self.request_id += 1
|
67
|
+
url = "#{api_url}?id=#{request_id}"
|
68
|
+
url << "&sid=#{sid}" if sid
|
69
|
+
debug "POST #{url}"
|
70
|
+
debug "#{body.inspect}"
|
71
|
+
response = HTTPClient.new.post url, [body].to_json, timeout: api_request_timeout
|
72
|
+
debug "#{response.code}\n#{response.body}"
|
73
|
+
resp = JSON.parse(response.body).first
|
74
|
+
raise "Error code received: #{resp}" if error_response?(resp)
|
75
|
+
resp
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Rmega
|
2
|
+
class Storage
|
3
|
+
|
4
|
+
attr_reader :session
|
5
|
+
|
6
|
+
def initialize session
|
7
|
+
@session = session
|
8
|
+
end
|
9
|
+
|
10
|
+
def logger
|
11
|
+
Rmega.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# Quota-related methods
|
16
|
+
|
17
|
+
def used_space
|
18
|
+
quota['cstrg']
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_space
|
22
|
+
quota['mstrg']
|
23
|
+
end
|
24
|
+
|
25
|
+
def quota
|
26
|
+
session.request a: 'uq', strg: 1
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Nodes finders
|
31
|
+
|
32
|
+
def nodes
|
33
|
+
nodes = session.request a: 'f', c: 1
|
34
|
+
nodes['f'].map { |node_data| Node.new(session, node_data) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def nodes_by_type type
|
38
|
+
nodes.select { |n| n.type == type }
|
39
|
+
end
|
40
|
+
|
41
|
+
def nodes_by_name name_regexp
|
42
|
+
nodes.select do |node|
|
43
|
+
node.name and node.name =~ name_regexp
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def trash_node
|
48
|
+
@trash ||= nodes_by_type(:trash).first
|
49
|
+
end
|
50
|
+
|
51
|
+
def root_node
|
52
|
+
@root_node ||= nodes_by_type(:root).first
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Handle node download
|
57
|
+
|
58
|
+
def self.chunks size
|
59
|
+
list = {}
|
60
|
+
p = 0
|
61
|
+
pp = 0
|
62
|
+
i = 1
|
63
|
+
|
64
|
+
while i <= 8 and p < size - (i * 0x20000)
|
65
|
+
list[p] = i * 0x20000
|
66
|
+
pp = p
|
67
|
+
p += list[p]
|
68
|
+
i += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
while p < size
|
72
|
+
list[p] = 0x100000
|
73
|
+
pp = p
|
74
|
+
p += list[p]
|
75
|
+
end
|
76
|
+
|
77
|
+
if size - pp > 0
|
78
|
+
list[pp] = size - pp
|
79
|
+
end
|
80
|
+
list
|
81
|
+
end
|
82
|
+
|
83
|
+
def download public_url, path
|
84
|
+
Node.initialize_by_public_url(session, public_url).download path
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Handle file upload
|
89
|
+
|
90
|
+
def upload_url filesize
|
91
|
+
session.request(a: 'u', s: filesize)['p']
|
92
|
+
end
|
93
|
+
|
94
|
+
def upload_chunk url, start, chunk
|
95
|
+
response = HTTPClient.new.post "#{url}/#{start}", chunk, timeout: Rmega.options.upload_timeout
|
96
|
+
response.body
|
97
|
+
end
|
98
|
+
|
99
|
+
def upload local_path, parent_node = root_node
|
100
|
+
local_path = File.expand_path local_path
|
101
|
+
filesize = File.size local_path
|
102
|
+
upld_url = upload_url filesize
|
103
|
+
|
104
|
+
ul_key = Array.new(6).map { rand(0..0xFFFFFFFF) }
|
105
|
+
aes_key = ul_key[0..3]
|
106
|
+
nonce = ul_key[4..5]
|
107
|
+
local_file = File.open local_path, 'rb'
|
108
|
+
file_handle = nil
|
109
|
+
file_mac = [0, 0, 0, 0]
|
110
|
+
|
111
|
+
Utils.show_progress :upload, filesize
|
112
|
+
|
113
|
+
self.class.chunks(filesize).each do |chunk_start, chunk_size|
|
114
|
+
buffer = local_file.read chunk_size
|
115
|
+
|
116
|
+
# TODO: should be (chunk_start/0x1000000000) >>> 0, (chunk_start/0x10) >>> 0
|
117
|
+
nonce = [nonce[0], nonce[1], (chunk_start/0x1000000000) >> 0, (chunk_start/0x10) >> 0]
|
118
|
+
|
119
|
+
encrypted_buffer = Crypto::AesCtr.encrypt aes_key, nonce, buffer
|
120
|
+
chunk_mac = encrypted_buffer[:mac]
|
121
|
+
|
122
|
+
file_handle = upload_chunk upld_url, chunk_start, encrypted_buffer[:data]
|
123
|
+
|
124
|
+
file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
|
125
|
+
file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
|
126
|
+
file_mac = Crypto::Aes.encrypt ul_key[0..3], file_mac
|
127
|
+
Utils.show_progress :upload, filesize, chunk_size
|
128
|
+
end
|
129
|
+
|
130
|
+
attribs = {n: File.basename(local_path)}
|
131
|
+
encrypt_attribs = Utils.a32_to_base64 Crypto.encrypt_attributes(ul_key[0..3], attribs)
|
132
|
+
|
133
|
+
meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
|
134
|
+
|
135
|
+
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
|
136
|
+
ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
|
137
|
+
|
138
|
+
encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
|
139
|
+
session.request a: 'p', t: parent_node.handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}]
|
140
|
+
|
141
|
+
nil
|
142
|
+
ensure
|
143
|
+
local_file.close if local_file
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/rmega/utils.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def show_progress direction, total, increment = 0
|
6
|
+
return unless Rmega.options.show_progress
|
7
|
+
@progressbar_progress = 0 if increment.zero?
|
8
|
+
@progressbar_progress += increment
|
9
|
+
format = "#{direction.to_s.capitalize} in progress #{Utils.format_bytes(@progressbar_progress)} of #{Utils.format_bytes(total)} | %P% | %e "
|
10
|
+
@progressbar ||= ProgressBar.create format: format, total: total
|
11
|
+
@progressbar.reset if increment.zero?
|
12
|
+
@progressbar.format format
|
13
|
+
@progressbar.progress += increment
|
14
|
+
end
|
15
|
+
|
16
|
+
def format_bytes bytes, round = 2
|
17
|
+
units = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB']
|
18
|
+
e = (bytes == 0 ? 0 : Math.log(bytes)) / Math.log(1024)
|
19
|
+
value = bytes.to_f / (1024 ** e.floor)
|
20
|
+
"#{value.round(round)}#{units[e]}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def str_to_a32 string
|
24
|
+
pad_to = string.bytesize + ((string.bytesize) % 4)
|
25
|
+
string = string.ljust pad_to, "\x00"
|
26
|
+
string.unpack 'l>*'
|
27
|
+
end
|
28
|
+
|
29
|
+
def a32_to_str a32, len=nil
|
30
|
+
if len
|
31
|
+
b = []
|
32
|
+
len.times do |i|
|
33
|
+
# TODO: should be ((a32[i>>2] >>> (24-(i & 3)*8)) & 255)
|
34
|
+
b << (((a32[i>>2] || 0) >> (24-(i & 3)*8)) & 255)
|
35
|
+
end
|
36
|
+
b.pack 'C*'
|
37
|
+
else
|
38
|
+
a32.pack 'l>*'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def b64a
|
43
|
+
@b64a ||= ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ["-", "_", "="]
|
44
|
+
end
|
45
|
+
|
46
|
+
def a32_to_base64 a32
|
47
|
+
base64urlencode a32_to_str(a32)
|
48
|
+
end
|
49
|
+
|
50
|
+
def base64_to_a32 base64
|
51
|
+
str_to_a32 base64urldecode(base64)
|
52
|
+
end
|
53
|
+
|
54
|
+
def base64urlencode string
|
55
|
+
i = 0
|
56
|
+
tmp_arr = []
|
57
|
+
|
58
|
+
while i < string.size + 1
|
59
|
+
o1 = string[i].ord rescue 0
|
60
|
+
i += 1
|
61
|
+
o2 = string[i].ord rescue 0
|
62
|
+
i += 1
|
63
|
+
o3 = string[i].ord rescue 0
|
64
|
+
i += 1
|
65
|
+
|
66
|
+
bits = o1 << 16 | o2 << 8 | o3
|
67
|
+
|
68
|
+
h1 = bits >> 18 & 0x3f
|
69
|
+
h2 = bits >> 12 & 0x3f
|
70
|
+
h3 = bits >> 6 & 0x3f
|
71
|
+
h4 = bits & 0x3f
|
72
|
+
|
73
|
+
tmp_arr.push b64a[h1] + b64a[h2] + b64a[h3] + b64a[h4]
|
74
|
+
end
|
75
|
+
|
76
|
+
enc = tmp_arr.join ''
|
77
|
+
r = string.size % 3
|
78
|
+
(r != 0) ? enc[0..r - 4] : enc
|
79
|
+
end
|
80
|
+
|
81
|
+
def base64urldecode data
|
82
|
+
data += '=='[((2-data.length*3)&3)..-1]
|
83
|
+
|
84
|
+
i = 0
|
85
|
+
ac = 0
|
86
|
+
dec = ""
|
87
|
+
tmp_arr = []
|
88
|
+
|
89
|
+
return data unless data
|
90
|
+
|
91
|
+
while i < data.size
|
92
|
+
h1 = b64a.index(data[i]) || -1
|
93
|
+
i += 1
|
94
|
+
h2 = b64a.index(data[i]) || -1
|
95
|
+
i += 1
|
96
|
+
h3 = b64a.index(data[i]) || -1
|
97
|
+
i += 1
|
98
|
+
h4 = b64a.index(data[i]) || -1
|
99
|
+
i += 1
|
100
|
+
|
101
|
+
bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4
|
102
|
+
|
103
|
+
o1 = bits >> 16 & 0xff
|
104
|
+
o2 = bits >> 8 & 0xff
|
105
|
+
o3 = bits & 0xff
|
106
|
+
|
107
|
+
if h3 == 64
|
108
|
+
tmp_arr[ac] = o1.chr
|
109
|
+
elsif h4 == 64
|
110
|
+
tmp_arr[ac] = o1.chr + o2.chr
|
111
|
+
else
|
112
|
+
tmp_arr[ac] = o1.chr + o2.chr + o3.chr
|
113
|
+
end
|
114
|
+
|
115
|
+
ac += 1
|
116
|
+
end
|
117
|
+
|
118
|
+
tmp_arr.join ''
|
119
|
+
end
|
120
|
+
|
121
|
+
def mpi2b s
|
122
|
+
bn = 1
|
123
|
+
r = [0]
|
124
|
+
rn = 0
|
125
|
+
sb = 256
|
126
|
+
sn = s.size
|
127
|
+
bm = 268435455
|
128
|
+
c = nil
|
129
|
+
|
130
|
+
return 0 if sn < 2
|
131
|
+
|
132
|
+
len = (sn - 2) * 8
|
133
|
+
bits = s[0].ord * 256 + s[1].ord
|
134
|
+
|
135
|
+
return 0 if bits > len or bits < len - 8
|
136
|
+
|
137
|
+
len.times do |n|
|
138
|
+
sb = sb << 1
|
139
|
+
|
140
|
+
if sb > 255
|
141
|
+
sb = 1
|
142
|
+
c = s[sn -= 1].ord
|
143
|
+
end
|
144
|
+
|
145
|
+
if bn > bm
|
146
|
+
bn = 1
|
147
|
+
r[rn += 1] = 0
|
148
|
+
end
|
149
|
+
|
150
|
+
if (c & sb) and (c & sb != 0)
|
151
|
+
r[rn] = r[rn] ? (r[rn] | bn) : bn
|
152
|
+
end
|
153
|
+
|
154
|
+
bn = bn << 1
|
155
|
+
end
|
156
|
+
r
|
157
|
+
end
|
158
|
+
|
159
|
+
def b2s b
|
160
|
+
bs = 28
|
161
|
+
bm = 268435455
|
162
|
+
bn = 1; bc = 0; r = [0]; rb = 1; rn = 0
|
163
|
+
bits = b.length * bs
|
164
|
+
rr = ''
|
165
|
+
|
166
|
+
bits.times do |n|
|
167
|
+
if (b[bc] & bn) and (b[bc] & bn) != 0
|
168
|
+
r[rn] = r[rn] ? (r[rn] | rb) : rb
|
169
|
+
end
|
170
|
+
|
171
|
+
rb = rb << 1
|
172
|
+
|
173
|
+
if rb > 255
|
174
|
+
rb = 1
|
175
|
+
r[rn += 1] = 0
|
176
|
+
end
|
177
|
+
|
178
|
+
bn = bn << 1
|
179
|
+
|
180
|
+
if bn > bm
|
181
|
+
bn = 1
|
182
|
+
bc += 1
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
while rn >= 0 && r[rn] == 0
|
187
|
+
rn -= 1
|
188
|
+
end
|
189
|
+
|
190
|
+
(rn + 1).times do |n|
|
191
|
+
rr = r[n].chr + rr
|
192
|
+
end
|
193
|
+
|
194
|
+
rr
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|