rmega 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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