Module: Megar::Crypto

Included in:
Session
Defined in:
lib/megar/crypto.rb

Overview

A straight-forward “quirks-mode” transcoding of core crypto methods required to talk to Mega. Some of this reeks a bit .. maybe more idiomatic ruby approaches are possible.

Generally we’re using signed 32-bit by default here … I don’t think it’s necessary, but it makes comparison with the javascript implementation easier.

Javascript reference implementations quoted here are taken from the Mega javascript source.

Instance Method Summary collapse

Instance Method Details

#a32_to_base64(a) ⇒ Object

Returns a base64-encoding given an array a of 32-bit integers

Javascript reference implementation: function a32_to_base64(a)



126
127
128
# File 'lib/megar/crypto.rb', line 126

def a32_to_base64(a)
  base64urlencode(a32_to_str(a))
end

#a32_to_str(a) ⇒ Object

Returns a packed string given an array a of 32-bit signed integers

Javascript reference implementation: function a32_to_str(a)



104
105
106
107
108
# File 'lib/megar/crypto.rb', line 104

def a32_to_str(a)
  b = ''
  (a.size * 4).times { |i| b << ((a[i>>2] >> (24-(i & 3)*8)) & 255).chr }
  b
end

#aes_cbc_decrypt(data, key) ⇒ Object

Returns AES-128 decrypted given key and data (String)



79
80
81
82
83
84
85
86
87
88
# File 'lib/megar/crypto.rb', line 79

def aes_cbc_decrypt(data, key)
  aes = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
  aes.decrypt
  aes.padding = 0
  aes.key = key
  aes.iv = "\0" * 16
  d = aes.update(data)
  d = aes.final if d.empty?
  d
end

#aes_cbc_decrypt_a32(data, key) ⇒ Object

Returns AES-128 decrypted given key and data (arrays of 32-bit signed integers)



74
75
76
# File 'lib/megar/crypto.rb', line 74

def aes_cbc_decrypt_a32(data, key)
  str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
end

#aes_encrypt_a32(data, key) ⇒ Object

Returns AES-128 encrypted given key and data (arrays of 32-bit signed integers)



62
63
64
65
66
67
68
69
70
71
# File 'lib/megar/crypto.rb', line 62

def aes_encrypt_a32(data, key)
  aes = OpenSSL::Cipher::Cipher.new('AES-128-ECB')
  aes.encrypt
  aes.padding = 0
  aes.key = key.pack('l>*')
  aes.update(data.pack('l>*')).unpack('l>*')
  # e = aes.update(data.pack('l>*')).unpack('l>*')
  # e << aes.final
  # e.unpack('l>*')
end

#base64_mpi_to_a32(s) ⇒ Object

Returns multiple precision integer (MPI) as an array of 32-bit signed integers decoded from base64 string s



215
216
217
# File 'lib/megar/crypto.rb', line 215

def base64_mpi_to_a32(s)
  mpi_to_a32(base64urldecode(s))
end

#base64_mpi_to_bn(s) ⇒ Object

Returns multiple precision integer (MPI) as a big integers decoded from base64 string s



221
222
223
224
225
# File 'lib/megar/crypto.rb', line 221

def base64_mpi_to_bn(s)
  data = base64urldecode(s)
  len = ((data[0].ord * 256 + data[1].ord + 7) / 8) + 2
  data[2,len+2].unpack('H*').first.to_i(16)
end

#base64_to_a32(s) ⇒ Object

Returns an array a of 32-bit integers given a base64-encoded b (String)

Javascript reference implementation: function base64_to_a32(s)



134
135
136
# File 'lib/megar/crypto.rb', line 134

def base64_to_a32(s)
  str_to_a32(base64urldecode(s))
end

#base64urldecode(data) ⇒ Object

Returns a string given data (base64-encoded String)

Javascript reference implementation: function base64urldecode(data)



150
151
152
# File 'lib/megar/crypto.rb', line 150

def base64urldecode(data)
  Base64.urlsafe_decode64(data + '=' * ((4 - data.length % 4) % 4))
end

#base64urlencode(data) ⇒ Object

Returns a base64-encoding given data (String).

Javascript reference implementation: function base64urlencode(data)



142
143
144
# File 'lib/megar/crypto.rb', line 142

def base64urlencode(data)
  Base64.urlsafe_encode64(data).gsub(/=*$/,'')
end

#decompose_file_key(key) ⇒ Object



389
390
391
392
393
394
395
396
# File 'lib/megar/crypto.rb', line 389

def decompose_file_key(key)
  [
    key[0] ^ key[4],
    key[1] ^ key[5],
    key[2] ^ key[6],
    key[3] ^ key[7]
  ]
end

#decompose_rsa_private_key(key) ⇒ Object

Returns the 4-part RSA key as array of big integers [d, p, q, u] given key (String)

result = p: The first factor of n, the RSA modulus result = q: The second factor of n result = d: The private exponent. result = u: The CRT coefficient, equals to (1/p) mod q.

Javascript reference implementation: function api_getsid2(res,ctx)



262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/megar/crypto.rb', line 262

def decompose_rsa_private_key(key)
  privk = key.dup
  decomposed_key = []
  offset = 0
  4.times do |i|
    len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
    privk_part = privk[0,len]
    # puts "\nl: ", len
    # puts "decrypted rsa part hex: \n", privk_part.unpack('H*').first
    decomposed_key << privk_part[2,privk_part.length].unpack('H*').first.to_i(16)
    privk.slice!(0,len)
  end
  decomposed_key
end

#decompose_rsa_private_key_a32(key) ⇒ Object

Returns the 4-part RSA key as 32-bit signed integers [d, p, q, u] given key (String)

result = p: The first factor of n, the RSA modulus result = q: The second factor of n result = d: The private exponent. result = u: The CRT coefficient, equals to (1/p) mod q.

Javascript reference implementation: function api_getsid2(res,ctx)



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/megar/crypto.rb', line 237

def decompose_rsa_private_key_a32(key)
  privk = key.dup
  decomposed_key = []
  # puts "decomp: privk.len:#{privk.length}"
  4.times do
    len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
    privk_part = privk[0,len]
    # puts "\nprivk_part #{base64urlencode(privk_part)}"
    privk_part_a32 = mpi_to_a32(privk_part)
    decomposed_key << privk_part_a32
    # puts "decomp: len:#{len} privk_part_a32:#{privk_part_a32.length} first:#{privk_part_a32.first} last:#{privk_part_a32.last}"
    privk.slice!(0,len)
  end
  decomposed_key
end

#decrypt_base64_to_a32(data, key) ⇒ Object

Returns decrypted array of 32-bit integers representation of base64 data decrypted using key



51
52
53
# File 'lib/megar/crypto.rb', line 51

def decrypt_base64_to_a32(data,key)
  decrypt_key(base64_to_a32(data), key)
end

#decrypt_base64_to_str(data, key) ⇒ Object

Returns decrypted string representation of base64 data decrypted using key



56
57
58
# File 'lib/megar/crypto.rb', line 56

def decrypt_base64_to_str(data,key)
  a32_to_str(decrypt_base64_to_a32(data, key))
end

#decrypt_file_attributes(f, key) ⇒ Object



383
384
385
386
387
# File 'lib/megar/crypto.rb', line 383

def decrypt_file_attributes(f,key)
  k = f['t'] == 0 ? decompose_file_key(key) : key
  rstr = aes_cbc_decrypt(base64urldecode(f['a']), a32_to_str(k))
  JSON.parse( rstr.gsub("\x0",'').gsub(/^.*{/,'{'))
end

#decrypt_file_key(f) ⇒ Object



378
379
380
381
# File 'lib/megar/crypto.rb', line 378

def decrypt_file_key(f)
  key = f['k'].split(':')[1]
  decrypt_key(base64_to_a32(key), self.master_key)
end

#decrypt_key(a, key) ⇒ Object

Returns a decrypted given an array a of 32-bit integers and key

Javascript reference implementation: function decrypt_key(cipher,a)



42
43
44
45
46
47
48
# File 'lib/megar/crypto.rb', line 42

def decrypt_key(a, key)
  b=[]
  (0..(a.length-1)).step(4) do |i|
    b.concat aes_cbc_decrypt_a32(a[i,4], key)
  end
  b
end

#decrypt_session_id(csid, rsa_private_key) ⇒ Object

Returns the decrypted session id given base64 MPI csid and RSA rsa_private_key as array of big integers [d, p, q, u]

Javascript reference implementation: function api_getsid2(res,ctx)



281
282
283
284
285
286
287
288
# File 'lib/megar/crypto.rb', line 281

def decrypt_session_id(csid,rsa_private_key)
  csid_bn = base64_mpi_to_bn(csid)
  sid_bn = rsa_decrypt(csid_bn,rsa_private_key)
  sid_hs = sid_bn.to_s(16)
  sid_hs = '0' + sid_hs if sid_hs.length % 2 > 0
  sid = hexstr_to_bstr(sid_hs)[0,43]
  base64urlencode(sid)
end

#hexstr_to_bstr(h) ⇒ Object

Returns a binary string given a string h of hex digits



372
373
374
375
376
# File 'lib/megar/crypto.rb', line 372

def hexstr_to_bstr(h)
  bstr = ''
  (0..h.length-1).step(2) {|n| bstr << h[n,2].to_i(16).chr }
  bstr
end

#mpi_to_a32(s) ⇒ Object

Returns multiple precision integer (MPI) as an array of 32-bit unsigned integers decoded from raw string s This first 16-bits of the MPI is the MPI length in bits

Javascript reference implementation: function mpi2b(s)



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/megar/crypto.rb', line 159

def mpi_to_a32(s)
  bs=28
  bx2=1<<bs
  bm=bx2-1

  bn=1
  r=[0]
  rn=0
  sb=256
  c = nil
  sn=s.length
  return 0 if(sn < 2)

  len=(sn-2)*8
  bits=s[0].ord*256+s[1].ord

  return 0 if(bits > len || bits < len-8)

  len.times do |n|
    if ((sb<<=1) > 255)
      sb=1
      sn -= 1
      c=s[sn].ord
    end
    if(bn > bm)
      bn=1
      rn += 1
      r << 0
    end
    if(c & sb != 0)
      r[rn]|=bn
    end
    bn<<=1
  end
  r
end

#openssl_rsa_cipher(pqdu) ⇒ Object

Returns an OpenSSL RSA cipher object initialised with pqdu (array of integer cipher components) p: The first factor of n, the RSA modulus q: The second factor of n d: The private exponent. u: The CRT coefficient, equals to (1/p) mod q.

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



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/megar/crypto.rb', line 354

def openssl_rsa_cipher(pqdu)
  rsa = OpenSSL::PKey::RSA.new
  p, q, d, u = pqdu
  rsa.p, rsa.q, rsa.d = p, q, d
  rsa.n = rsa.p * rsa.q
  # # dmp1 = d mod (p-1)
  # rsa.dmp1 = rsa.d % (rsa.p - 1)
  # # dmq1 = d mod (q-1)
  # rsa.dmq1 = rsa.d % (rsa.q - 1)
  # # iqmp = q^-1 mod p?
  # rsa.iqmp =  (rsa.q ** -1) % rsa.p
  # # ipmq =  (rsa.p ** -1) % rsa.q
  # ipmq =  rsa.p ** -1 % rsa.q
  rsa.e = 0 # 65537
  rsa
end

#openssl_rsa_decrypt(m, pqdu) ⇒ Object

Returns the private key decryption of m given pqdu (array of integer cipher components) This implementation uses OpenSSL RSA public key feature.

NB: can’t get this to work exactly right with Mega yet



334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/megar/crypto.rb', line 334

def openssl_rsa_decrypt(m, pqdu)
  rsa = openssl_rsa_cipher(pqdu)

  chunk_size = 256 # hmm. need to figure out how to calc for "data greater than mod len"
  # number.size(self.n) - 1 : Return the maximum number of bits that can be handled by this key.
  decrypt_texts = []
  (0..m.length - 1).step(chunk_size) do |i|
    pt_part = m[i,chunk_size]
    decrypt_texts << rsa.private_decrypt(pt_part,3)
  end
  decrypt_texts.join
end

#prepare_key(a) ⇒ Object

Returns encrypted key given an array a of 32-bit integers

Javascript reference implementation: function prepare_key(a)



18
19
20
21
22
23
24
25
26
27
28
# File 'lib/megar/crypto.rb', line 18

def prepare_key(a)
  pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
  0x10000.times do
    (0..(a.length-1)).step(4) do |j|
      key = [0,0,0,0]
      4.times {|i| key[i] = a[i+j] if (i+j < a.length) }
      pkey = aes_encrypt_a32(pkey,key)
    end
  end
  pkey
end

#prepare_key_pw(password) ⇒ Object

Returns encrypted key given the plain-text password string

Javascript reference implementation: function prepare_key_pw(password)



34
35
36
# File 'lib/megar/crypto.rb', line 34

def prepare_key_pw(password)
  prepare_key(str_to_a32(password))
end

#rsa_decrypt(m, pqdu) ⇒ Object

Returns the private key decryption of m given pqdu (array of integer cipher components). Computes m**d (mod n).

This implementation uses a Pure Ruby implementation of RSA private_decrypt

p: The first factor of n, the RSA modulus q: The second factor of n d: The private exponent. u: The CRT coefficient, equals to (1/p) mod q.

n = pq n is used as the modulus for both the public and private keys. Its length, usually expressed in bits, is the key length.

φ(n) = (p – 1)(q – 1), where φ is Euler’s totient function.

Choose an integer e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1; i.e., e and φ(n) are coprime. e is released as the public key exponent

Determine d as d ≡ e−1 (mod φ(n)), i.e., d is the multiplicative inverse of e (modulo φ(n)). d is kept as the private key exponent.

More info: en.wikipedia.org/wiki/RSA_(algorithm)#Operation

Javascript reference implementation: function RSAdecrypt(m, d, p, q, u)



315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/megar/crypto.rb', line 315

def rsa_decrypt(m, pqdu)
  p, q, d, u = pqdu
  if p && q && u
    m1 = Math.powm(m, d % (p-1), p)
    m2 = Math.powm(m, d % (q-1), q)
    h = m2 - m1
    h = h + q if h < 0
    h = h*u % q
    h*p+m1
  else
    Math.powm(m, d, p*q)
  end
end

#str_to_a32(b) ⇒ Object

Returns an array of 32-bit signed integers representing the string b

Javascript reference implementation: function str_to_a32(b)



94
95
96
97
98
# File 'lib/megar/crypto.rb', line 94

def str_to_a32(b)
  a = Array.new((b.length+3) >> 2,0)
  b.length.times { |i| a[i>>2] |= (b.getbyte(i) << (24-(i & 3)*8)) }
  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
end

#stringhash(s, aeskey) ⇒ Object

Returns a base64-encoding of string s hashed with aeskey key

Javascript reference implementation: function stringhash(s,aes)



114
115
116
117
118
119
120
# File 'lib/megar/crypto.rb', line 114

def stringhash(s,aeskey)
  s32 = str_to_a32(s)
  h32 = [0,0,0,0]
  s32.length.times {|i| h32[i&3] ^= s32[i] }
  16384.times {|i| h32 = aes_encrypt_a32(h32, aeskey) }
  a32_to_base64([h32[0],h32[2]])
end