megar 0.0.1 → 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.
@@ -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