rmega 0.0.2
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.
- 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
|