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