megar 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,422 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ # A straight-forward "quirks-mode" transcoding of core cryptographic methods required to talk to Mega.
5
+ # Some of this reeks a bit .. maybe more idiomatic ruby approaches are possible.
6
+ #
7
+ # Generally we're using signed 32-bit by default here ... I don't think it's necessary, but it makes comparison with
8
+ # the javascript implementation easier.
9
+ #
10
+ # Javascript reference implementations quoted here are taken from the Mega javascript source.
11
+ #
12
+ module Megar::CryptoSupport
13
+
14
+ # Verifies that the required crypto support is available from ruby/openssl
15
+ def crypto_requirements_met?
16
+ OpenSSL::Cipher.ciphers.include?("AES-128-CTR")
17
+ end
18
+
19
+ # Returns encrypted key given an array +a+ of 32-bit integers
20
+ #
21
+ # Javascript reference implementation: function prepare_key(a)
22
+ #
23
+ def prepare_key(a)
24
+ pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
25
+ 0x10000.times do
26
+ (0..(a.length-1)).step(4) do |j|
27
+ key = [0,0,0,0]
28
+ 4.times {|i| key[i] = a[i+j] if (i+j < a.length) }
29
+ pkey = aes_encrypt_a32(pkey,key)
30
+ end
31
+ end
32
+ pkey
33
+ end
34
+
35
+ # Returns encrypted key given the plain-text +password+ string
36
+ #
37
+ # Javascript reference implementation: function prepare_key_pw(password)
38
+ #
39
+ def prepare_key_pw(password)
40
+ prepare_key(str_to_a32(password))
41
+ end
42
+
43
+ # Returns a decrypted given an array +a+ of 32-bit integers and +key+
44
+ #
45
+ # Javascript reference implementation: function decrypt_key(cipher,a)
46
+ #
47
+ def decrypt_key(a, key)
48
+ b=[]
49
+ (0..(a.length-1)).step(4) do |i|
50
+ b.concat aes_cbc_decrypt_a32(a[i,4], key)
51
+ end
52
+ b
53
+ end
54
+
55
+ # Returns decrypted array of 32-bit integers representation of base64 +data+ decrypted using +key+
56
+ def decrypt_base64_to_a32(data,key)
57
+ decrypt_key(base64_to_a32(data), key)
58
+ end
59
+
60
+ # Returns decrypted string representation of base64 +data+ decrypted using +key+
61
+ def decrypt_base64_to_str(data,key)
62
+ a32_to_str(decrypt_base64_to_a32(data, key))
63
+ end
64
+
65
+
66
+ # Returns AES-128 encrypted given +key+ and +data+ (arrays of 32-bit signed integers)
67
+ def aes_encrypt_a32(data, key)
68
+ aes = OpenSSL::Cipher::Cipher.new('AES-128-ECB')
69
+ aes.encrypt
70
+ aes.padding = 0
71
+ aes.key = key.pack('l>*')
72
+ aes.update(data.pack('l>*')).unpack('l>*')
73
+ # e = aes.update(data.pack('l>*')).unpack('l>*')
74
+ # e << aes.final
75
+ # e.unpack('l>*')
76
+ end
77
+
78
+ # Returns AES-128 decrypted given +key+ and +data+ (arrays of 32-bit signed integers)
79
+ def aes_cbc_decrypt_a32(data, key)
80
+ str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
81
+ end
82
+
83
+ # Returns AES-128 decrypted given +key+ and +data+ (String)
84
+ def aes_cbc_decrypt(data, key)
85
+ aes = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
86
+ aes.decrypt
87
+ aes.padding = 0
88
+ aes.key = key
89
+ aes.iv = "\0" * 16
90
+ d = aes.update(data)
91
+ d = aes.final if d.empty?
92
+ d
93
+ end
94
+
95
+ # Returns an array of 32-bit signed integers representing the string +b+
96
+ #
97
+ # Javascript reference implementation: function str_to_a32(b)
98
+ #
99
+ def str_to_a32(b)
100
+ a = Array.new((b.length+3) >> 2,0)
101
+ b.length.times { |i| a[i>>2] |= (b.getbyte(i) << (24-(i & 3)*8)) }
102
+ a.pack('l>*').unpack('l>*') # hack to force to signed 32-bit ... I don't think we really need to do this, but it makes comparison with
103
+ end
104
+
105
+ # Returns a packed string given an array +a+ of 32-bit signed integers
106
+ #
107
+ # Javascript reference implementation: function a32_to_str(a)
108
+ #
109
+ def a32_to_str(a)
110
+ b = ''
111
+ (a.size * 4).times { |i| b << ((a[i>>2] >> (24-(i & 3)*8)) & 255).chr }
112
+ b
113
+ end
114
+
115
+ # Returns a base64-encoding of string +s+ hashed with +aeskey+ key
116
+ #
117
+ # Javascript reference implementation: function stringhash(s,aes)
118
+ #
119
+ def stringhash(s,aeskey)
120
+ s32 = str_to_a32(s)
121
+ h32 = [0,0,0,0]
122
+ s32.length.times {|i| h32[i&3] ^= s32[i] }
123
+ 16384.times {|i| h32 = aes_encrypt_a32(h32, aeskey) }
124
+ a32_to_base64([h32[0],h32[2]])
125
+ end
126
+
127
+ # Returns a base64-encoding given an array +a+ of 32-bit integers
128
+ #
129
+ # Javascript reference implementation: function a32_to_base64(a)
130
+ #
131
+ def a32_to_base64(a)
132
+ base64urlencode(a32_to_str(a))
133
+ end
134
+
135
+ # Returns an array +a+ of 32-bit integers given a base64-encoded +b+ (String)
136
+ #
137
+ # Javascript reference implementation: function base64_to_a32(s)
138
+ #
139
+ def base64_to_a32(s)
140
+ str_to_a32(base64urldecode(s))
141
+ end
142
+
143
+ # Returns a base64-encoding given +data+ (String).
144
+ #
145
+ # Javascript reference implementation: function base64urlencode(data)
146
+ #
147
+ def base64urlencode(data)
148
+ Base64.urlsafe_encode64(data).gsub(/=*$/,'')
149
+ end
150
+
151
+ # Returns a string given +data+ (base64-encoded String)
152
+ #
153
+ # Javascript reference implementation: function base64urldecode(data)
154
+ #
155
+ def base64urldecode(data)
156
+ Base64.urlsafe_decode64(data + '=' * ((4 - data.length % 4) % 4))
157
+ end
158
+
159
+ # Returns multiple precision integer (MPI) as an array of 32-bit unsigned integers decoded from raw string +s+
160
+ # This first 16-bits of the MPI is the MPI length in bits
161
+ #
162
+ # Javascript reference implementation: function mpi2b(s)
163
+ #
164
+ def mpi_to_a32(s)
165
+ bs=28
166
+ bx2=1<<bs
167
+ bm=bx2-1
168
+
169
+ bn=1
170
+ r=[0]
171
+ rn=0
172
+ sb=256
173
+ c = nil
174
+ sn=s.length
175
+ return 0 if(sn < 2)
176
+
177
+ len=(sn-2)*8
178
+ bits=s[0].ord*256+s[1].ord
179
+
180
+ return 0 if(bits > len || bits < len-8)
181
+
182
+ len.times do |n|
183
+ if ((sb<<=1) > 255)
184
+ sb=1
185
+ sn -= 1
186
+ c=s[sn].ord
187
+ end
188
+ if(bn > bm)
189
+ bn=1
190
+ rn += 1
191
+ r << 0
192
+ end
193
+ if(c & sb != 0)
194
+ r[rn]|=bn
195
+ end
196
+ bn<<=1
197
+ end
198
+ r
199
+ end
200
+
201
+ # Alternative mpi2b implementation; doesn't quite match the javascript implementation yet however
202
+ # def native_mpi_to_a32(s)
203
+ # len = s.length - 2
204
+ # short = len % 4
205
+ # base = len - short
206
+ # r = s[2,base].unpack('N*')
207
+ # case short
208
+ # when 1
209
+ # r.concat s[2+base,short].unpack('C*')
210
+ # when 2
211
+ # r.concat s[2+base,short].unpack('n*')
212
+ # when 3
213
+ # r.concat ("\0" + s[2+base,short]).unpack('N*')
214
+ # end
215
+ # r
216
+ # end
217
+
218
+ # Returns multiple precision integer (MPI) as an array of 32-bit signed integers decoded from base64 string +s+
219
+ #
220
+ def base64_mpi_to_a32(s)
221
+ mpi_to_a32(base64urldecode(s))
222
+ end
223
+
224
+ # Returns multiple precision integer (MPI) as a big integers decoded from base64 string +s+
225
+ #
226
+ def base64_mpi_to_bn(s)
227
+ data = base64urldecode(s)
228
+ len = ((data[0].ord * 256 + data[1].ord + 7) / 8) + 2
229
+ data[2,len+2].unpack('H*').first.to_i(16)
230
+ end
231
+
232
+
233
+ # Returns the 4-part RSA key as 32-bit signed integers [d, p, q, u] given +key+ (String)
234
+ #
235
+ # result[0] = p: The first factor of n, the RSA modulus
236
+ # result[1] = q: The second factor of n
237
+ # result[2] = d: The private exponent.
238
+ # result[3] = u: The CRT coefficient, equals to (1/p) mod q.
239
+ #
240
+ # Javascript reference implementation: function api_getsid2(res,ctx)
241
+ #
242
+ def decompose_rsa_private_key_a32(key)
243
+ privk = key.dup
244
+ decomposed_key = []
245
+ # puts "decomp: privk.len:#{privk.length}"
246
+ 4.times do
247
+ len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
248
+ privk_part = privk[0,len]
249
+ # puts "\nprivk_part #{base64urlencode(privk_part)}"
250
+ privk_part_a32 = mpi_to_a32(privk_part)
251
+ decomposed_key << privk_part_a32
252
+ # puts "decomp: len:#{len} privk_part_a32:#{privk_part_a32.length} first:#{privk_part_a32.first} last:#{privk_part_a32.last}"
253
+ privk.slice!(0,len)
254
+ end
255
+ decomposed_key
256
+ end
257
+
258
+ # Returns the 4-part RSA key as array of big integers [d, p, q, u] given +key+ (String)
259
+ #
260
+ # result[0] = p: The first factor of n, the RSA modulus
261
+ # result[1] = q: The second factor of n
262
+ # result[2] = d: The private exponent.
263
+ # result[3] = u: The CRT coefficient, equals to (1/p) mod q.
264
+ #
265
+ # Javascript reference implementation: function api_getsid2(res,ctx)
266
+ #
267
+ def decompose_rsa_private_key(key)
268
+ privk = key.dup
269
+ decomposed_key = []
270
+ offset = 0
271
+ 4.times do |i|
272
+ len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
273
+ privk_part = privk[0,len]
274
+ # puts "\nl: ", len
275
+ # puts "decrypted rsa part hex: \n", privk_part.unpack('H*').first
276
+ decomposed_key << privk_part[2,privk_part.length].unpack('H*').first.to_i(16)
277
+ privk.slice!(0,len)
278
+ end
279
+ decomposed_key
280
+ end
281
+
282
+ # Returns the decrypted session id given base64 MPI +csid+ and RSA +rsa_private_key+ as array of big integers [d, p, q, u]
283
+ #
284
+ # Javascript reference implementation: function api_getsid2(res,ctx)
285
+ #
286
+ def decrypt_session_id(csid,rsa_private_key)
287
+ csid_bn = base64_mpi_to_bn(csid)
288
+ sid_bn = rsa_decrypt(csid_bn,rsa_private_key)
289
+ sid_hs = sid_bn.to_s(16)
290
+ sid_hs = '0' + sid_hs if sid_hs.length % 2 > 0
291
+ sid = hexstr_to_bstr(sid_hs)[0,43]
292
+ base64urlencode(sid)
293
+ end
294
+
295
+ # Returns the private key decryption of +m+ given +pqdu+ (array of integer cipher components).
296
+ # Computes m**d (mod n).
297
+ #
298
+ # This implementation uses a Pure Ruby implementation of RSA private_decrypt
299
+ #
300
+ # p: The first factor of n, the RSA modulus
301
+ # q: The second factor of n
302
+ # d: The private exponent.
303
+ # u: The CRT coefficient, equals to (1/p) mod q.
304
+ #
305
+ # n = pq
306
+ # n is used as the modulus for both the public and private keys. Its length, usually expressed in bits, is the key length.
307
+ #
308
+ # φ(n) = (p – 1)(q – 1), where φ is Euler's totient function.
309
+ #
310
+ # Choose an integer e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1; i.e., e and φ(n) are coprime.
311
+ # e is released as the public key exponent
312
+ #
313
+ # Determine d as d ≡ e−1 (mod φ(n)), i.e., d is the multiplicative inverse of e (modulo φ(n)).
314
+ # d is kept as the private key exponent.
315
+ #
316
+ # More info: http://en.wikipedia.org/wiki/RSA_(algorithm)#Operation
317
+ #
318
+ # Javascript reference implementation: function RSAdecrypt(m, d, p, q, u)
319
+ #
320
+ def rsa_decrypt(m, pqdu)
321
+ p, q, d, u = pqdu
322
+ if p && q && u
323
+ m1 = Math.powm(m, d % (p-1), p)
324
+ m2 = Math.powm(m, d % (q-1), q)
325
+ h = m2 - m1
326
+ h = h + q if h < 0
327
+ h = h*u % q
328
+ h*p+m1
329
+ else
330
+ Math.powm(m, d, p*q)
331
+ end
332
+ end
333
+
334
+
335
+ # Returns the private key decryption of +m+ given +pqdu+ (array of integer cipher components)
336
+ # This implementation uses OpenSSL RSA public key feature.
337
+ #
338
+ # NB: can't get this to work exactly right with Mega yet
339
+ def openssl_rsa_decrypt(m, pqdu)
340
+ rsa = openssl_rsa_cipher(pqdu)
341
+
342
+ chunk_size = 256 # hmm. need to figure out how to calc for "data greater than mod len"
343
+ # number.size(self.n) - 1 : Return the maximum number of bits that can be handled by this key.
344
+ decrypt_texts = []
345
+ (0..m.length - 1).step(chunk_size) do |i|
346
+ pt_part = m[i,chunk_size]
347
+ decrypt_texts << rsa.private_decrypt(pt_part,3)
348
+ end
349
+ decrypt_texts.join
350
+ end
351
+
352
+ # Returns an OpenSSL RSA cipher object initialised with +pqdu+ (array of integer cipher components)
353
+ # p: The first factor of n, the RSA modulus
354
+ # q: The second factor of n
355
+ # d: The private exponent.
356
+ # u: The CRT coefficient, equals to (1/p) mod q.
357
+ #
358
+ # NB: this hacks the RSA object creation n a way that should work, but can't get this to work exactly right with Mega yet
359
+ def openssl_rsa_cipher(pqdu)
360
+ rsa = OpenSSL::PKey::RSA.new
361
+ p, q, d, u = pqdu
362
+ rsa.p, rsa.q, rsa.d = p, q, d
363
+ rsa.n = rsa.p * rsa.q
364
+ # # dmp1 = d mod (p-1)
365
+ # rsa.dmp1 = rsa.d % (rsa.p - 1)
366
+ # # dmq1 = d mod (q-1)
367
+ # rsa.dmq1 = rsa.d % (rsa.q - 1)
368
+ # # iqmp = q^-1 mod p?
369
+ # rsa.iqmp = (rsa.q ** -1) % rsa.p
370
+ # # ipmq = (rsa.p ** -1) % rsa.q
371
+ # ipmq = rsa.p ** -1 % rsa.q
372
+ rsa.e = 0 # 65537
373
+ rsa
374
+ end
375
+
376
+ # Returns a binary string given a string +h+ of hex digits
377
+ def hexstr_to_bstr(h)
378
+ bstr = ''
379
+ (0..h.length-1).step(2) {|n| bstr << h[n,2].to_i(16).chr }
380
+ bstr
381
+ end
382
+
383
+ # Returns a decrypted file key given +f+ (API file response)
384
+ def decrypt_file_key(f)
385
+ key = f['k'].split(':')[1]
386
+ decrypt_key(base64_to_a32(key), self.master_key)
387
+ end
388
+
389
+ # Returns decrypted file attributes given encrypted +attributes+ and decomposed file +key+
390
+ #
391
+ # Javascript reference implementation: function dec_attr(attr,key)
392
+ #
393
+ def decrypt_file_attributes(attributes,key)
394
+ rstr = aes_cbc_decrypt(base64urldecode(attributes), a32_to_str(key))
395
+ JSON.parse( rstr.gsub("\x0",'').gsub(/^.*{/,'{'))
396
+ end
397
+
398
+ # Returns a decomposed file +key+
399
+ #
400
+ # Javascript reference implementation: function startdownload2(res,ctx)
401
+ #
402
+ def decompose_file_key(key)
403
+ [
404
+ key[0] ^ key[4],
405
+ key[1] ^ key[5],
406
+ key[2] ^ key[6],
407
+ key[3] ^ key[7]
408
+ ]
409
+ end
410
+
411
+ # Returns AES CTR-mode decryption cipher
412
+ #
413
+ def get_file_decrypter(key,iv)
414
+ aes = OpenSSL::Cipher::Cipher.new('AES-128-CTR')
415
+ aes.decrypt
416
+ aes.padding = 0
417
+ aes.key = a32_to_str(key)
418
+ aes.iv = a32_to_str(iv)
419
+ aes
420
+ end
421
+
422
+ end
@@ -3,6 +3,9 @@ module Megar
3
3
  # A general Megar exception
4
4
  class Error < StandardError; end
5
5
 
6
+ # Raised when crypto requirements are not met by the ruby platform we're running on
7
+ class CryptoSupportRequirementsError < Error; end
8
+
6
9
  class MegaRequestError < Error
7
10
 
8
11
  # Initialise with +error_code+ returned from Mega
@@ -1,6 +1,6 @@
1
1
  class Megar::Session
2
2
 
3
- include Megar::Crypto
3
+ include Megar::CryptoSupport
4
4
  include Megar::Connection
5
5
 
6
6
  attr_accessor :options
@@ -21,6 +21,14 @@ class Megar::Session
21
21
  # autoconnect: true/false -- performs immediate login if true (default)
22
22
  #
23
23
  def initialize(options={})
24
+ unless crypto_requirements_met?
25
+ raise Megar::CryptoSupportRequirementsError.new(%(
26
+ Oops! Looks like we don't have the necessary OpenSSL support available.
27
+
28
+ See https://github.com/tardate/megar/blob/master/README.rdoc for hints on how to
29
+ make sure the correct OpenSSL version is linked with your ruby.
30
+ \n))
31
+ end
24
32
  default_options = { autoconnect: true }
25
33
  @options = default_options.merge(options.symbolize_keys)
26
34
  self.api_endpoint = @options[:api_endpoint] if @options[:api_endpoint]
@@ -80,8 +88,14 @@ class Megar::Session
80
88
  end
81
89
 
82
90
  def reset_files!
83
- @folders = Megar::Folders.new
84
- @files = Megar::Files.new
91
+ @folders = Megar::Folders.new(session: self)
92
+ @files = Megar::Files.new(session: self)
93
+ end
94
+
95
+ # Command: requests file download url from the API for the file with id +node_id+
96
+ def get_file_download_url_response(node_id)
97
+ ensure_connected!
98
+ api_request({'a' => 'g', 'g' => 1, 'n' => node_id})
85
99
  end
86
100
 
87
101
  protected
@@ -91,10 +105,24 @@ class Megar::Session
91
105
  raise "Not connected" unless connected?
92
106
  end
93
107
 
108
+ # Command: sends login request to the API
94
109
  def get_login_response
95
110
  api_request({'a' => 'us', 'user' => email, 'uh' => uh})
96
111
  end
97
112
 
113
+ # Returns the encoded user password key
114
+ def password_key
115
+ prepare_key_pw(password)
116
+ end
117
+
118
+ # Returns the calculated uh parameter based on email and password
119
+ #
120
+ # Javascript reference implementation: function stringhash(s,aes)
121
+ #
122
+ def uh
123
+ stringhash(email.downcase, password_key)
124
+ end
125
+
98
126
  # Command: decrypt/decode the login +response_data+ received from Mega
99
127
  #
100
128
  # Javascript reference implementation: function api_getsid2(res,ctx)
@@ -113,6 +141,7 @@ class Megar::Session
113
141
  end
114
142
  end
115
143
 
144
+ # Command: requests file/folder node listing from the API
116
145
  def get_files_response
117
146
  ensure_connected!
118
147
  api_request({'a' => 'f', 'c' => 1})
@@ -127,11 +156,12 @@ class Megar::Session
127
156
  case f['t']
128
157
  when 0 # File
129
158
  item_attributes[:key] = k = decrypt_file_key(f)
130
- item_attributes[:attributes] = decrypt_file_attributes(f,k)
159
+ item_attributes[:decomposed_key] = key = decompose_file_key(k)
160
+ item_attributes[:attributes] = decrypt_file_attributes(f['a'],key)
131
161
  files.add(item_attributes)
132
162
  when 1 # Folder
133
163
  item_attributes[:key] = k = decrypt_file_key(f)
134
- item_attributes[:attributes] = decrypt_file_attributes(f,k)
164
+ item_attributes[:attributes] = decrypt_file_attributes(f['a'],k)
135
165
  folders.add(item_attributes)
136
166
  when 2,3,4 # Root, Inbox, Trash Bin
137
167
  folders.add(item_attributes)
@@ -140,18 +170,4 @@ class Megar::Session
140
170
  true
141
171
  end
142
172
 
143
- # Returns the encoded user password key
144
- def password_key
145
- prepare_key_pw(password)
146
- end
147
-
148
- # Returns the calculated uh parameter based on email and password
149
- #
150
- # Javascript reference implementation: function stringhash(s,aes)
151
- #
152
- def uh
153
- stringhash(email.downcase, password_key)
154
- end
155
-
156
-
157
173
  end