megar 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ 0.0.3 put and get ready 9-Mar-2013
2
+ ===========================================================
3
+ * basic file upload support added
4
+
1
5
  0.0.2 little more hearty release 2-Mar-2013
2
6
  ===========================================================
3
7
  * basic file download support added
@@ -12,9 +12,7 @@ Consider this totally alpha at this point, especially since the Mega API has yet
12
12
 
13
13
  * Currently tested with MRI 1.9.3
14
14
  * Requires OpenSSL 1.0.1+ (and properly linked with ruby)
15
- * MRI 1.9.2 and 2.x not yet tested
16
- * Rubinius and JRuby 1.9 modes not yet tested
17
- * No plans to support 1.8 Ruby branches
15
+ * MRI 1.9.2 and 2.x, Rubinius and JRuby 1.9 modes not yet tested. No plans to support 1.8 Ruby modes.
18
16
 
19
17
  Mega API coverage is far from complete at this point (help appreciated!). Here's the run-down:
20
18
 
@@ -22,7 +20,11 @@ Supported API functions:
22
20
  * User/session management: Complete accounts
23
21
  * Login session challenge/response
24
22
  * Retrieve file and folder nodes
25
- * Request download URL (and download file contents)
23
+ * Request download URL
24
+ * Download files
25
+ * Download file message authentication codes ({MAC}[http://en.wikipedia.org/wiki/Message_authentication_code])
26
+ * Request upload URL
27
+ * Upload files
26
28
 
27
29
  Currently unsupported API functions:
28
30
  * User/session management: Ephemeral accounts
@@ -35,7 +37,6 @@ Currently unsupported API functions:
35
37
  * Create/delete public handle
36
38
  * Create/modify/delete outgoing share
37
39
  * Key handling - Set or request share/node keys
38
- * Request upload URL
39
40
  * Add/update user
40
41
  * Get user
41
42
  * Retrieve user's public key
@@ -198,6 +199,35 @@ Once you have a file object (e.g. <tt>session.files.first</tt>):
198
199
  file = session.files.first
199
200
  file.body # returns the full decrypted body content
200
201
 
202
+ === How to upload a file
203
+
204
+ Uploading a new file is performed using the <tt>create</tt> method of a folder of the files collection. For example:
205
+
206
+ # get a handle to the file you want to upload e.g.
207
+ file_handle = Pathname.new('../path/to/my_file.png')
208
+
209
+ # create the file in the root (Cloud Drive) folder:
210
+ session.root.create( body: file_handle )
211
+
212
+ # or this also implicitly creates the file in the root folder:
213
+ session.files.create( body: file_handle )
214
+
215
+ # or to create the file in a specific folder (e.g. 'My Images'):
216
+ session.folders.find_by_name('My Images').create( body: file_handle )
217
+
218
+ The <tt>file_handle</tt> can actually be any suitable file name, Pathname or File object.
219
+
220
+ If you want to change the name of the file as stored in Mega, you can also provide a <tt>name</tt> parameter:
221
+
222
+ session.files.create( name: 'with a new name.png', body: file_handle )
223
+
224
+ Once a file has been uploaded successfully, it will also be available in the local files catalog:
225
+
226
+ session.files.create( body: file_handle, name: 'new_file.png' )
227
+ => true
228
+ session.files.find_by_name('new_file.png')
229
+ => returns new file catalog entry
230
+
201
231
 
202
232
  === CLI: How to use the command-line client
203
233
 
@@ -211,6 +241,9 @@ All operations require credentials to ba passed on the command line (email and p
211
241
  Connecting to mega as my@email.com..
212
242
  Connected!
213
243
 
244
+ Note: If you just want to do a little hacking around, {megar_rt}[https://github.com/tardate/megar_rt]
245
+ is an easy way to bootstrap an isolated environment and be up and running super fast.
246
+
214
247
 
215
248
  === CLI: How to get a file listing
216
249
 
@@ -228,13 +261,31 @@ Use the <tt>get</tt> command:
228
261
 
229
262
  $ megar --email=my@email.com --password=mypassword get L0xHwayA
230
263
  Connecting to mega as my@email.com..
231
- Downloading L0xHwayA to mega.png...
264
+ Downloading L0xHwayA to mega.png..
232
265
 
233
266
  Note: this is very simple interface at the moment:
234
267
  * you can only identify the file by the ID (not name)
235
268
  * it downloads and saves using the same name as on the server
236
269
  * and it doesn't check if you are overwriting a local file
237
- * no file integrity testing yet
270
+
271
+
272
+ === CLI: How to upload a file
273
+
274
+ Use the <tt>put</tt> command:
275
+
276
+ $ megar --email=my@email.com --password=mypassword put ../path/to/my_file.png
277
+ Connecting to mega as my@email.com..
278
+ Uploading my_file.png..
279
+
280
+ This uploads a file into the root folder by default, and keeps the same file name.
281
+
282
+ Standard command-line expansions work, so to upload multiple files, you can do this:
283
+
284
+ $ megar --email=my@email.com --password=mypassword put *.png
285
+ Connecting to mega as my@email.com..
286
+ Uploading my_file_1.png..
287
+ Uploading my_file_2.png..
288
+ (etc for all files matching *.png)
238
289
 
239
290
 
240
291
  === How to run tests
@@ -1 +1,2 @@
1
- require 'megar/adapters/file_downloader'
1
+ require 'megar/adapters/file_downloader'
2
+ require 'megar/adapters/file_uploader'
@@ -29,16 +29,18 @@ class Megar::FileDownloader
29
29
  def content
30
30
  return unless live_session?
31
31
  decoded_content = ''
32
+ calculated_mac = [0, 0, 0, 0]
33
+
34
+ decryptor = get_file_decrypter(decomposed_key,iv)
32
35
 
33
- # TODO here: init mac calculation (perhaps should be an option to use of not)
34
- decryptor = session.get_file_decrypter(file.decomposed_key,iv)
35
36
  get_chunks(download_size).each do |chunk_start, chunk_size|
36
37
  chunk = stream.readpartial(chunk_size)
37
38
  decoded_chunk = decryptor.update(chunk)
38
39
  decoded_content << decoded_chunk
39
- # TODO here: calculate chunk mac
40
+ calculated_mac = accumulate_mac(decoded_chunk,calculated_mac,decomposed_key,chunk_mac_iv)
40
41
  end
41
- # TODO here: perform integrity check against expected file mac
42
+
43
+ raise Megar::MacVerificationError.new unless ([calculated_mac[0] ^ calculated_mac[1], calculated_mac[2] ^ calculated_mac[3]] == mac)
42
44
 
43
45
  decoded_content
44
46
  end
@@ -62,13 +64,32 @@ class Megar::FileDownloader
62
64
  # Returns the decrypted download attributes
63
65
  def download_attributes
64
66
  if attributes = download_url_response['at']
65
- decrypt_file_attributes(attributes,file.decomposed_key)
67
+ decrypt_file_attributes(attributes,decomposed_key)
66
68
  end
67
69
  end
68
70
 
69
71
  # Returns the initialisation vector
70
72
  def iv
71
- @iv ||= file.key[4,2] + [0, 0]
73
+ @iv ||= key[4,2] + [0, 0]
74
+ end
75
+
76
+ def chunk_mac_iv
77
+ [iv[0], iv[1], iv[0], iv[1]]
78
+ end
79
+
80
+ # Returns the expected MAC for the file
81
+ def mac
82
+ key[6,2]
83
+ end
84
+
85
+ # Returns the file key (shortcut)
86
+ def key
87
+ file.key
88
+ end
89
+
90
+ # Returns the file key (shortcut)
91
+ def decomposed_key
92
+ file.decomposed_key
72
93
  end
73
94
 
74
95
  # Returns the initial value for AES counter
@@ -92,41 +113,4 @@ class Megar::FileDownloader
92
113
  !!(session && file)
93
114
  end
94
115
 
95
- # Returns an array of chunk sizes given total file +size+
96
- #
97
- # Chunk boundaries are located at the following positions:
98
- # 0 / 128K / 384K / 768K / 1280K / 1920K / 2688K / 3584K / 4608K / ... (every 1024 KB) / EOF
99
- def get_chunks(size)
100
- chunks = []
101
- p = pp = 0
102
- i = 1
103
-
104
- while i <= 8 and p < size - i * 0x20000 do
105
- chunk_size = i * 0x20000
106
- chunks << [p, chunk_size]
107
- pp = p
108
- p += chunk_size
109
- i += 1
110
- end
111
-
112
- while p < size - 0x100000 do
113
- chunk_size = 0x100000
114
- chunks << [p, chunk_size]
115
- pp = p
116
- p += chunk_size
117
- end
118
-
119
- chunks << [p, size - p] if p < size
120
-
121
- chunks
122
- end
123
-
124
- def calculate_chunk_mac(iv,chunk)
125
- chunk_mac = [iv[0], iv[1], iv[0], iv[1]]
126
- (1..chunk.length).take(16).each do |bit|
127
- # NYI
128
- end
129
- chunk_mac
130
- end
131
-
132
116
  end
@@ -0,0 +1,148 @@
1
+ require 'open-uri'
2
+ require 'net/http'
3
+ require 'pathname'
4
+
5
+ # Encapsulates a file upload task. This is intended as a one-shot helper.
6
+ #
7
+ # NB: there seems to be an issue if you try to upload
8
+ # multiple files of exactly the same size in the same session.
9
+ #
10
+ # Javascript reference implementation: function initupload3()
11
+ #
12
+ class Megar::FileUploader
13
+ include Megar::CryptoSupport
14
+
15
+ attr_reader :folder
16
+ attr_reader :session
17
+ attr_reader :body
18
+ attr_writer :name
19
+
20
+ def initialize(options={})
21
+ @folder = options[:folder]
22
+ @session = @folder && @folder.session
23
+ self.body = options[:body]
24
+ self.name = options[:name]
25
+ end
26
+
27
+ def body=(value)
28
+ @body = case value
29
+ when File
30
+ value
31
+ when Pathname
32
+ File.open(value,'rb')
33
+ when String
34
+ if value.size < 1024 # theoretically, a path name could be even longer but we'll assume
35
+ # see if its a file name
36
+ File.open(value,'rb')
37
+ end
38
+ when NilClass
39
+ else
40
+ raise Megar::UnsupportedFileHandleTypeError.new
41
+ end
42
+ end
43
+
44
+ # Returns the size of the file content
45
+ def upload_size
46
+ body.size
47
+ end
48
+
49
+ # Returns stream handle to the file body
50
+ def stream
51
+ body
52
+ end
53
+
54
+ # Returns the name of the file
55
+ def name
56
+ @name ||= Pathname.new(body.path).basename.to_s
57
+ end
58
+
59
+ # Command: perform upload
60
+ def post!
61
+ return unless live_session?
62
+ calculated_mac = [0, 0, 0, 0]
63
+ completion_file_handle = ''
64
+
65
+ encryptor = get_file_encrypter(upload_key,iv_str)
66
+
67
+ get_chunks(upload_size).each do |chunk_start, chunk_size|
68
+ chunk = stream.readpartial(chunk_size)
69
+ encrypted_chunk = encryptor.update(chunk)
70
+ calculated_mac = accumulate_mac(chunk,calculated_mac,mac_encryption_key,mac_iv,false)
71
+ completion_file_handle = post_chunk(encrypted_chunk,chunk_start)
72
+ end
73
+ stream.close
74
+ meta_mac = [calculated_mac[0] ^ calculated_mac[1], calculated_mac[2] ^ calculated_mac[3]]
75
+
76
+ upload_attributes_response = send_file_upload_attributes(meta_mac,completion_file_handle)
77
+ if upload_attributes_response.is_a?(Hash) && upload_attributes_response['f']
78
+ session.handle_files_response(upload_attributes_response,false)
79
+ else
80
+ raise Megar::FileUploadError.new
81
+ end
82
+ end
83
+
84
+ # upload chunk
85
+ def post_chunk(encrypted_chunk,chunk_start)
86
+ Net::HTTP.start(upload_uri.host, upload_uri.port) { |http|
87
+ path = "#{upload_uri.path}/#{chunk_start}"
88
+ response = http.post(path,encrypted_chunk)
89
+ response.body
90
+ }
91
+ end
92
+ protected :post_chunk
93
+
94
+ def send_file_upload_attributes(meta_mac,completion_file_handle)
95
+ session.send_file_upload_attributes(folder.id,name,upload_key,meta_mac,completion_file_handle)
96
+ end
97
+ protected :send_file_upload_attributes
98
+
99
+ # Returns an upload url for the file content
100
+ def upload_url
101
+ upload_url_response['p']
102
+ end
103
+
104
+ # Returns an upload url for the file content as a URI
105
+ def upload_uri
106
+ @upload_uri ||= URI.parse(upload_url)
107
+ end
108
+
109
+ def upload_key
110
+ @upload_key ||= 6.times.each_with_object([]) {|i,memo| memo << rand( 0xFFFFFFFF) }
111
+ end
112
+
113
+ # Returns the encryption key to use for calculating the MAC
114
+ def mac_encryption_key
115
+ upload_key[0,4]
116
+ end
117
+
118
+ # Returns the initialisation vector as array of 32bit integer to use for calculating the MAC
119
+ def mac_iv
120
+ [upload_key[4], upload_key[5], upload_key[4], upload_key[5]]
121
+ end
122
+
123
+ def iv
124
+ ((upload_key[4]<<32)+upload_key[5])<<64
125
+ end
126
+
127
+ def iv_str
128
+ hexstr_to_bstr( iv.to_s(16) )
129
+ end
130
+
131
+ # Returns and caches a file upload response
132
+ def upload_url_response
133
+ @upload_url_response ||= if live_session?
134
+ session.get_file_upload_url_response(upload_size)
135
+ else
136
+ {}
137
+ end
138
+ end
139
+
140
+ protected
141
+
142
+ # Returns true if live session/folder properly set
143
+ def live_session?
144
+ !!(session && folder)
145
+ end
146
+
147
+
148
+ end
@@ -42,10 +42,17 @@ module Megar::CatalogItem
42
42
  # 4: Special node: Trash Bin
43
43
  attr_accessor :type
44
44
 
45
+ # Returns the default parent folder (default is nil)
46
+ def default_parent_folder
47
+ end
48
+
45
49
  # Returns a handle to the enclosing folder (if any)
46
50
  def parent_folder
47
- if session && parent_folder_id
51
+ return unless session
52
+ if parent_folder_id
48
53
  session.folders.find_by_id(parent_folder_id)
54
+ else
55
+ default_parent_folder
49
56
  end
50
57
  end
51
58
 
@@ -2,4 +2,26 @@
2
2
  class Megar::Files
3
3
  include Megar::CatalogItem
4
4
 
5
+ # Command: creates a new file given +attributes+ which contains the following elements:
6
+ # body: the file body. May be a Pathname, File, or filename (String)
7
+ # name: the file name to assign (optional if already available from the body object)
8
+ #
9
+ # The file is stored in the parent folder (or root folder by defult)
10
+ def create(attributes)
11
+ if uploader = self.uploader(attributes)
12
+ uploader.post!
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ # Returns the default parent folder (default is root)
19
+ def default_parent_folder
20
+ session && session.folders.root
21
+ end
22
+
23
+ def uploader(attributes)
24
+ Megar::FileUploader.new(attributes.merge(folder: parent_folder))
25
+ end
26
+
5
27
  end
@@ -29,4 +29,21 @@ class Megar::Folder
29
29
  session.files.find_all_by_parent_folder_id(id)
30
30
  end
31
31
 
32
+ # Command: creates a new file given +attributes+ which contains the following elements:
33
+ # body: the file body. May be a Pathname, File, or filename (String)
34
+ # name: the file name to assign (optional if already available from the body object)
35
+ #
36
+ # The file is stored in the parent folder (or root folder by defult)
37
+ def create(attributes)
38
+ if uploader = self.uploader(attributes)
39
+ uploader.post!
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def uploader(attributes)
46
+ Megar::FileUploader.new(attributes.merge(folder: self))
47
+ end
48
+
32
49
  end
@@ -52,6 +52,16 @@ module Megar::CryptoSupport
52
52
  b
53
53
  end
54
54
 
55
+ # Javascript reference implementation: function encrypt_key(cipher,a)
56
+ #
57
+ def encrypt_key(a, key)
58
+ b=[]
59
+ (0..(a.length-1)).step(4) do |i|
60
+ b.concat aes_cbc_encrypt_a32(a[i,4], key)
61
+ end
62
+ b
63
+ end
64
+
55
65
  # Returns decrypted array of 32-bit integers representation of base64 +data+ decrypted using +key+
56
66
  def decrypt_base64_to_a32(data,key)
57
67
  decrypt_key(base64_to_a32(data), key)
@@ -75,12 +85,12 @@ module Megar::CryptoSupport
75
85
  # e.unpack('l>*')
76
86
  end
77
87
 
78
- # Returns AES-128 decrypted given +key+ and +data+ (arrays of 32-bit signed integers)
88
+ # Returns AES-128 CBC decrypted given +key+ and +data+ (arrays of 32-bit signed integers)
79
89
  def aes_cbc_decrypt_a32(data, key)
80
90
  str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
81
91
  end
82
92
 
83
- # Returns AES-128 decrypted given +key+ and +data+ (String)
93
+ # Returns AES-128 CBC decrypted given +key+ and +data+ (String)
84
94
  def aes_cbc_decrypt(data, key)
85
95
  aes = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
86
96
  aes.decrypt
@@ -92,14 +102,36 @@ module Megar::CryptoSupport
92
102
  d
93
103
  end
94
104
 
105
+ # Returns AES-128 CBC decrypted given +key+ and +data+ (arrays of 32-bit signed integers)
106
+ def aes_cbc_encrypt_a32(data, key, signed=true)
107
+ str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key)),signed)
108
+ end
109
+
110
+ # Returns AES-128 CBC encrypted given +key+ and +data+ (String)
111
+ def aes_cbc_encrypt(data, key)
112
+ aes = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
113
+ aes.encrypt
114
+ aes.padding = 0
115
+ aes.key = key
116
+ aes.iv = "\0" * 16
117
+ d = aes.update(data)
118
+ d = aes.final if d.empty?
119
+ d
120
+ end
121
+
95
122
  # Returns an array of 32-bit signed integers representing the string +b+
96
123
  #
97
124
  # Javascript reference implementation: function str_to_a32(b)
98
125
  #
99
- def str_to_a32(b)
126
+ def str_to_a32(b,signed=true)
100
127
  a = Array.new((b.length+3) >> 2,0)
101
128
  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
129
+ if signed
130
+ # hack to force to signed 32-bit ... I don't think we really need to do this, but it makes comparison with
131
+ a.pack('l>*').unpack('l>*')
132
+ else
133
+ a
134
+ end
103
135
  end
104
136
 
105
137
  # Returns a packed string given an array +a+ of 32-bit signed integers
@@ -395,6 +427,18 @@ module Megar::CryptoSupport
395
427
  JSON.parse( rstr.gsub("\x0",'').gsub(/^.*{/,'{'))
396
428
  end
397
429
 
430
+ # Returns encrypted file attributes given encrypted +attributes+ and decomposed file +key+
431
+ #
432
+ # Javascript reference implementation: function enc_attr(attr,key)
433
+ #
434
+ def encrypt_file_attributes(attributes, key)
435
+ rstr = 'MEGA' + attributes.to_json
436
+ if (mod = rstr.length % 16) > 0
437
+ rstr << "\x0" * (16 - mod)
438
+ end
439
+ aes_cbc_encrypt(rstr, a32_to_str(key))
440
+ end
441
+
398
442
  # Returns a decomposed file +key+
399
443
  #
400
444
  # Javascript reference implementation: function startdownload2(res,ctx)
@@ -408,7 +452,37 @@ module Megar::CryptoSupport
408
452
  ]
409
453
  end
410
454
 
411
- # Returns AES CTR-mode decryption cipher
455
+
456
+ # Returns an array of chunk sizes given total file +size+
457
+ #
458
+ # Chunk boundaries are located at the following positions:
459
+ # 0 / 128K / 384K / 768K / 1280K / 1920K / 2688K / 3584K / 4608K / ... (every 1024 KB) / EOF
460
+ def get_chunks(size)
461
+ chunks = []
462
+ p = pp = 0
463
+ i = 1
464
+
465
+ while i <= 8 and p < size - i * 0x20000 do
466
+ chunk_size = i * 0x20000
467
+ chunks << [p, chunk_size]
468
+ pp = p
469
+ p += chunk_size
470
+ i += 1
471
+ end
472
+
473
+ while p < size - 0x100000 do
474
+ chunk_size = 0x100000
475
+ chunks << [p, chunk_size]
476
+ pp = p
477
+ p += chunk_size
478
+ end
479
+
480
+ chunks << [p, size - p] if p < size
481
+
482
+ chunks
483
+ end
484
+
485
+ # Returns AES CTR-mode decryption cipher given +key+ and +iv+ as array of int
412
486
  #
413
487
  def get_file_decrypter(key,iv)
414
488
  aes = OpenSSL::Cipher::Cipher.new('AES-128-CTR')
@@ -419,4 +493,48 @@ module Megar::CryptoSupport
419
493
  aes
420
494
  end
421
495
 
496
+ # Returns AES CTR-mode encryption cipher given +key+ as array of int and +iv+ as binary string
497
+ #
498
+ def get_file_encrypter(key,iv)
499
+ aes = OpenSSL::Cipher::Cipher.new('AES-128-CTR')
500
+ aes.encrypt
501
+ aes.padding = 0
502
+ aes.key = a32_to_str(key)
503
+ aes.iv = iv
504
+ aes
505
+ end
506
+
507
+ # Returns the +chunk+ mac (array of unsigned int)
508
+ #
509
+ def calculate_chunk_mac(chunk,key,iv,signed=false)
510
+ chunk_mac = iv
511
+ (0..chunk.length-1).step(16).each do |i|
512
+ block = chunk[i,16]
513
+ if (mod = block.length % 16) > 0
514
+ block << "\x0" * (16 - mod)
515
+ end
516
+ block = str_to_a32(block,signed)
517
+ chunk_mac = [
518
+ chunk_mac[0] ^ block[0],
519
+ chunk_mac[1] ^ block[1],
520
+ chunk_mac[2] ^ block[2],
521
+ chunk_mac[3] ^ block[3]
522
+ ]
523
+ chunk_mac = aes_cbc_encrypt_a32(chunk_mac, key, signed)
524
+ end
525
+ chunk_mac
526
+ end
527
+
528
+ # Calculates the accummulated MAC given new +chunk+
529
+ def accumulate_mac(chunk,progressive_mac,key,iv,signed=true)
530
+ chunk_mac = calculate_chunk_mac(chunk,key,iv,signed)
531
+ combined_mac = [
532
+ progressive_mac[0] ^ chunk_mac[0],
533
+ progressive_mac[1] ^ chunk_mac[1],
534
+ progressive_mac[2] ^ chunk_mac[2],
535
+ progressive_mac[3] ^ chunk_mac[3]
536
+ ]
537
+ aes_cbc_encrypt_a32(combined_mac, key, signed)
538
+ end
539
+
422
540
  end
@@ -6,6 +6,15 @@ module Megar
6
6
  # Raised when crypto requirements are not met by the ruby platform we're running on
7
7
  class CryptoSupportRequirementsError < Error; end
8
8
 
9
+ # Raised when MAC fails verification test
10
+ class MacVerificationError < Error; end
11
+
12
+ # Raised when unsupported file type passed to create/update a file
13
+ class UnsupportedFileHandleTypeError < Error; end
14
+
15
+ # Raised on non-API related file upload errors
16
+ class FileUploadError < Error; end
17
+
9
18
  class MegaRequestError < Error
10
19
 
11
20
  # Initialise with +error_code+ returned from Mega
@@ -98,6 +98,57 @@ class Megar::Session
98
98
  api_request({'a' => 'g', 'g' => 1, 'n' => node_id})
99
99
  end
100
100
 
101
+ # Command: requests file upload url from the API for the file of +size+
102
+ def get_file_upload_url_response(size)
103
+ ensure_connected!
104
+ api_request({'a' => 'u', 's' => size})
105
+ end
106
+
107
+ # Command: sends updated attributes following file upload to the API
108
+ def send_file_upload_attributes(folder_id,name,upload_key,meta_mac,completion_file_handle)
109
+ attribs = {'n' => name}
110
+ encrypt_attribs = base64urlencode(encrypt_file_attributes(attribs, upload_key[0,4]))
111
+
112
+ key = [upload_key[0] ^ upload_key[4], upload_key[1] ^ upload_key[5],
113
+ upload_key[2] ^ meta_mac[0], upload_key[3] ^ meta_mac[1],
114
+ upload_key[4], upload_key[5], meta_mac[0], meta_mac[1]]
115
+
116
+ encrypted_key = a32_to_base64(encrypt_key(key, master_key))
117
+ api_request({
118
+ 'a' => 'p',
119
+ 't' => folder_id,
120
+ 'n' => [{
121
+ 'h' => completion_file_handle,
122
+ 't' => 0,
123
+ 'a' => encrypt_attribs,
124
+ 'k' => encrypted_key
125
+ }]
126
+ })
127
+ end
128
+
129
+ # Command: decrypt/decode the login +response_data+ received from Mega
130
+ #
131
+ def handle_files_response(response_data,reset=true)
132
+ reset_files! if reset
133
+ response_data['f'].each do |f|
134
+ item_attributes = {id: f['h'], payload: f.dup, type: f['t'] }
135
+ case f['t']
136
+ when 0 # File
137
+ item_attributes[:key] = k = decrypt_file_key(f)
138
+ item_attributes[:decomposed_key] = key = decompose_file_key(k)
139
+ item_attributes[:attributes] = decrypt_file_attributes(f['a'],key)
140
+ files.add(item_attributes)
141
+ when 1 # Folder
142
+ item_attributes[:key] = k = decrypt_file_key(f)
143
+ item_attributes[:attributes] = decrypt_file_attributes(f['a'],k)
144
+ folders.add(item_attributes)
145
+ when 2,3,4 # Root, Inbox, Trash Bin
146
+ folders.add(item_attributes)
147
+ end
148
+ end
149
+ true
150
+ end
151
+
101
152
  protected
102
153
 
103
154
  # Command: enforces guard condition requiring authenticated connection to proceed
@@ -147,27 +198,4 @@ class Megar::Session
147
198
  api_request({'a' => 'f', 'c' => 1})
148
199
  end
149
200
 
150
- # Command: decrypt/decode the login +response_data+ received from Mega
151
- #
152
- def handle_files_response(response_data)
153
- reset_files!
154
- response_data['f'].each do |f|
155
- item_attributes = {id: f['h'], payload: f.dup, type: f['t'] }
156
- case f['t']
157
- when 0 # File
158
- item_attributes[:key] = k = decrypt_file_key(f)
159
- item_attributes[:decomposed_key] = key = decompose_file_key(k)
160
- item_attributes[:attributes] = decrypt_file_attributes(f['a'],key)
161
- files.add(item_attributes)
162
- when 1 # Folder
163
- item_attributes[:key] = k = decrypt_file_key(f)
164
- item_attributes[:attributes] = decrypt_file_attributes(f['a'],k)
165
- folders.add(item_attributes)
166
- when 2,3,4 # Root, Inbox, Trash Bin
167
- folders.add(item_attributes)
168
- end
169
- end
170
- true
171
- end
172
-
173
201
  end
@@ -30,6 +30,8 @@ class Megar::Shell
30
30
  ls
31
31
  when /get/i
32
32
  get(args[1])
33
+ when /put/i
34
+ put(args.drop(1))
33
35
  else
34
36
  $stderr.puts "Connected!"
35
37
  end
@@ -63,10 +65,13 @@ Commands:
63
65
  (none) : will perform a basic connection test only
64
66
  ls : returns a full file listing
65
67
  get file_id : downloads the file with id file_id
68
+ put file_name : uploads the file called "file_name"
66
69
 
67
70
  Examples:
68
71
  megar --email=my@mail.com --password=MyPassword ls
69
72
  megar -e my@mail.com -p MyPassword ls
73
+ megar -e my@mail.com -p MyPassword get 74ZTXbyR
74
+ megar -e my@mail.com -p MyPassword put ../path/to/my_file.png
70
75
 
71
76
  EOS
72
77
  end
@@ -77,12 +82,14 @@ EOS
77
82
  self.class.usage
78
83
  end
79
84
 
85
+ # do file listing
80
86
  def ls
81
87
  session.files.each do |file|
82
88
  puts file
83
89
  end
84
90
  end
85
91
 
92
+ # download file with +file_id+
86
93
  def get(file_id)
87
94
  if file = session.files.find_by_id(file_id)
88
95
  $stderr.puts "Downloading #{file_id} to #{file.name}.."
@@ -94,6 +101,14 @@ EOS
94
101
  end
95
102
  end
96
103
 
104
+ # upload file(s) +filenames+
105
+ def put(filenames)
106
+ Array(filenames).each do |filename|
107
+ $stderr.puts "Uploading #{filename}.."
108
+ session.files.create(body: filename)
109
+ end
110
+ end
111
+
97
112
  def session
98
113
  @session ||= Megar::Session.new(email: email, password: password)
99
114
  end
@@ -1,3 +1,3 @@
1
1
  module Megar
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "email": "megartest@gmail.com",
3
3
  "email_mixed_case": "Megartest@gmail.com",
4
- "password": "ToLlw2D748TqCVAmrc8laio1",
4
+ "password": "NEvfEM5xME7jAnr6YN9JZH8M",
5
5
  "autoconnect": false,
6
6
  "login_response_data": {
7
- "csid": "CAA1bK6vGGR02BJCb8CfkaeI7F1kIUyvLSXZFXzlFIrsWIp0kw7GgCUZbqN_zE9z4_eV-g5Vl8GyC--TYwUZ1JigVwNPTWEncqk0qdUmmv1SpOcbOu-Z0Z5-GWnTK2oxSp8nKPBn0UGBvf98hQ3KD1uhjEFBrJbNdAgbHc8BGTaYvE0dHNtIE1Zz7bulDDg798Rjj1eGH5bC1vEZDQ_uI7l-wBRtXsCCdXAo6GZEyzr-dcpYInPL-Sy8ZY10RU67E-x_-v_sEUqyN69EHyENk97XzL0arGvXbnGNVH4OhdjynQ1csIlcmvbNQ5kAe9KjJTIT9NeVnmSAjao3AAbIfvRj",
7
+ "csid": "CABObd6kRiw5JF_4Fr7z2R_tleh8g3jlsiGl3fX48C3MFq57ni86x3cVOgtNgv48N5ZX_68g-MROc_5j6TP3g2FTBpOhMRf2E4swTAwBQxa0CF04dj6hfHKHIp5XyE1KPGN57GvecLzTV6FgtanY1v41Pm_1H6PE89XSejovQIGOOKwsmeUOlmfLYreMe58Il1aROqzr6E-eybD8BzozQgy8XMO80Gcc6eoja0VbbxehHPdCYS990tAWBYkG8up-rnQqwpln-UrmtZbXtv4WoNO5n3wI4s5abyQl7PbG_42xw9Uk2QYwvIrRSnt4rDB8m-iAihGS7Zp6QQO5RVpuWvU1",
8
8
  "privk": "4Iurk4vQ0BlLEEvEyaRUX8QnYzJFIedm0RVKe2pLh4wTzBBtgmafnszD_dP3Y40P-DSNLEGfWghL-0_BIW4XP8tfGNFkMg7mnYgQYC91ccz58BbhGEDf7-j97dstckf3OSSsT9D9H-cocXzyt1m231w9d7YLX23c3KAGz3vxdnl66F4jsFGO9AdIyb1KTEEt7NHkljtbz3WgWlk6W6wBawuEbJvLPHPx2L16i0iacbbkQi8ZF4pmddswMa9yWOSXwUribGDbdgx_jDGjBoKJUJI-7Aa-aOaRPEoVUTft2R2lNAVErvYwD_GIsIhQiCcJWul-KqPhI3vquvzUHd2rHHvKyTLyvXKyIDeVdBGXTaqFA2ni9xsjUNMOX14ftBZ4_fgDce0jvrZzYRs5IuOMt3K9gKMpBpUWtnlaBYbnYdL2zJuFlrM77PlcYab2uLLqlSe4hh-FxMAc_p_8tDAoU4dPgpgOpU3GRR8i7oszu_MKibzNpRBeAX1UtwKwQepjPnipu9aQlPy9OtgB-5KmGi34EcMKBPg8BN6vHRYg0Za_lk_TtTQ4WbkllAqR3Fla5-RBjzB7f3dWae4ES3VX4f-dI-UYrrKDQkqaS0R_xLaMPNG6_GQMDM7L5Fsa8I3hFjdgfs9rV4Xf_uIFrhUjm25EuT5JCdobNmwBi9VU5nCbk0LQJdrXDNYV3fmD-Y_p_KV8x_-opRnXUV03SCopOD6DhitsFtpv065QXcG1SNDu0mM-kgDh9E-QddtQQtXLLPuKFyX5o1ts08EqXTxHsZo5tIsevbExZoz9hxSRrr-V5swEDcJXoV78_k6sR3iniZ_m77JVo9QU572av2MvNhyu5YeTXyc0m6Me7cPGiO0",
9
- "k": "x1EvGzxu16FpfYajxFSFWQ"
9
+ "k": "0sdW36PBOcqicUJ7814isw"
10
10
  },
11
11
  "master_key": [
12
12
  384287193,
@@ -14,8 +14,8 @@
14
14
  554881366,
15
15
  530403344
16
16
  ],
17
- "expected_uh": "4h8kPReRUug",
18
- "sid": "tRUhX2shZiPr8hl2KRGXJ3pBOEN6Q2Y5dFp3lh5A8mZnlSFgTcLOChMkLg",
17
+ "expected_uh": "FeGJgLm4ciM",
18
+ "sid": "gVes6vS8WZ2c-CB7HJRdXHpBOEN6Q2Y5dFp3CQbFwprvxr0nkhb_XTqQrA",
19
19
  "rsa_private_key_b64": "BACOQ7VCjwkiwYCKhqRRvPTMBaQ5Yx2_sszj6ILrxrTBOJ2bu-9LsOSvaGz0HpnM2lXpJg3h6QWJdlQBOhwvfeafHg7rcEaaDuUWONifv5ABpQgeoAij6cgARMvFiOdjarMvu954Eduup8PHcgfjsBSu76qpu3UqkZCpBpByAYU1oQQA1i5oxuA9i7qsXxhJRsBeceI7hZSnNYAQzsWfFD0kDW8GsJBuK-CST6oNNp0z7MQ5Qof6DZ8x9wt5ZJR43HFmD2VtTQUZLvIeemC4URuR74UWn25CETyzQdIhdNSEibaCRvi0sNwb-ikCZVxxo4-O7ZBQPw0egrQOJW06HlCQUykH_00EIoiBRcbJLS6W3nJ8KhddDf_4glZYPz6fBpqu1jIdIBGOGQf4Ttn_DmdAgkLxWF7AF6k59D45J9kxfANRJIBGSWUsI1bQBlrwFtois4O14uQkQ40Jh_d5BCrLPe599zrliGH1SsECx_xYGWcWY_GPsPa99d_St56BB9oUtDW3A1clEx2uVS8OWqRPgVimu6MXs_TEEnjWGRFofHpl_8qGeAoPbNkXsTMQb6uUK4NCYq3UzVfreByEnOqxuQcu2jlF4Ey0UH9w_KjkRyT8xu--kyZAbktYaw4EZmGUxZIlkZLk_OAGaW8KWPZhdGxjfi8K7WeZK2HtLe4At48DC_ED_20pNtkTCrB6-FQlMj6a4O2C1sE6pSdrIPofcK-HpaefOXqYKWpRM-Wnp_qHnuJgEtgodQ-pw74nXkOsVFZoJzVGCepZKoa2xhevAu3CfXMfR97LIski17PPwWV9n7yQ7onSKDmJP-q4mO7PG7LICE22WkgJkk_T1W4UkDTkCxZE_t64F6JOryg",
20
20
  "decomposed_rsa_private_key": [
21
21
  99901518447147034909201847419097315968297147437151469508360346463543063455874100243913856315732601845631262435262260706834732781343251117295631282035669718956849603873760427510697419513879951677293141811802298231979984193675329310799949319780256427743430868153430092107435691279045586737323088097814348379553,
@@ -109,6 +109,16 @@
109
109
  "k": "zA8CzCf9tZw:cnpz0cav9fLAJHK1TbL3MD5n9R7VvedDwbFRVp4qQh4",
110
110
  "s": 137080,
111
111
  "ts": 1361925899
112
+ },
113
+ {
114
+ "h": "LpJwSL5A",
115
+ "p": "ywJVFBKB",
116
+ "u": "zA8CzCf9tZw",
117
+ "t": 0,
118
+ "a": "lw4tddDm8mFFKt4EuPkPkBlbf5sG9Xf47c-Bif0lJ7jH9xiU1JbQcxqdRixtsOgN",
119
+ "k": "zA8CzCf9tZw:bLCHGilxhwBlxkZs8uptY87wZf_WNCR6RefwEgtbRmo",
120
+ "s": 8920445,
121
+ "ts": 1362235380
112
122
  }
113
123
  ],
114
124
  "ok": [
@@ -124,14 +134,17 @@
124
134
  "m": "megartest@gmail.com"
125
135
  }
126
136
  ],
127
- "sn": "2q4ZpNDFGNA"
137
+ "sn": "rkC92G0-SNg"
138
+ },
139
+ "file_upload_url_response": {
140
+ "p": "http://gfs262n185.userstorage.mega.co.nz/ul/YKxC-sS4xNF8MgL4iddMm9PNukmzMODqpxWsV5CquftEe5YDUZaq2_QSuqt8ubYtt0_VYE-yLk2gN5uJIyDHhQ"
128
141
  },
129
142
  "sample_files": {
130
143
  "megar_test_sample_1.txt": {
131
144
  "file_download_url_response": {
132
145
  "s": 39,
133
146
  "at": "lv-LcMAl0dvpdxQFWNVZ7m9-mFT79mS0Mi_fH9InTqtBgcpe8kRVDGQ5Hj4BrXrc",
134
- "g": "http://gfs262n164.userstorage.mega.co.nz/dl/mri5tfEwyd0zbAImLAPBzTeWY8_A6olaAjn4rztgo3cQZ_ovfobz7A5X4vhHxU6xDWX3Gcxkozxb0acSEOyC-wRl7gO3jZrJzqLT13812fUfghQvwQ"
147
+ "g": "http://gfs262n164.userstorage.mega.co.nz/dl/Hv0Huu5AKOP1CQKwZNxpr7JMVXST1oxYEWT08YF3WIAWIDX4-3-bKAGVDU13ESYqBmcMDQGDsohpihtrDwEIpz3W5yvdTIFAhhWR49qpZvi3yfkhGg"
135
148
  },
136
149
  "key": [
137
150
  2080156050,
@@ -156,13 +169,17 @@
156
169
  0,
157
170
  0
158
171
  ],
172
+ "mac": [
173
+ -1644788558,
174
+ -1184834090
175
+ ],
159
176
  "initial_counter_value": 60129435948344049243162590180076945408
160
177
  },
161
178
  "megar_test_sample_2.png": {
162
179
  "file_download_url_response": {
163
180
  "s": 137080,
164
181
  "at": "OFTM18eQGxUTcJP2BQWXBo23vnjqs4b9QzmNZtDqekz4GZESVNiYlqKtasZqdVHN",
165
- "g": "http://gfs262n170.userstorage.mega.co.nz/dl/dpAQXYa5Q7qOQQKAWgqcNsmyPfr8As0DEJlX2LCKmTAvRVlvWpW6F3Hw-xV2Ou2ynF_bfjIcGG5ZO0vi3hf-6rFOx3jAu-FNNSKnvEA1irjUy4hT7g"
182
+ "g": "http://gfs262n170.userstorage.mega.co.nz/dl/ZWHZvT4lAtuWdwKYNzKGjqDfAvohHAi7AnKmQCCCWormdDhn-CLkuVb5Onah2GxtzcJ_vQFoI_EWAxIxGyBIXrsRLw60IJ6ZNjNWiDpcPG_6jTgP4A"
166
183
  },
167
184
  "key": [
168
185
  1029178532,
@@ -187,6 +204,10 @@
187
204
  0,
188
205
  0
189
206
  ],
207
+ "mac": [
208
+ 870048465,
209
+ -473559286
210
+ ],
190
211
  "initial_counter_value": 111383565294958830569229712241211736064
191
212
  }
192
213
  }
@@ -70,6 +70,7 @@ module CryptoExpectationsHelper
70
70
  e[:rsa_private_key_b64] = session.rsa_private_key_b64
71
71
  e[:decomposed_rsa_private_key] = session.decomposed_rsa_private_key
72
72
  e[:files_response_data] = session.send(:get_files_response)
73
+ e[:file_upload_url_response] = session.get_file_upload_url_response(39)
73
74
  session.send(:handle_files_response,e[:files_response_data])
74
75
  megar_test_sample_1 = session.files.find_by_name('megar_test_sample_1.txt')
75
76
  megar_test_sample_2 = session.files.find_by_name('megar_test_sample_2.png')
@@ -81,6 +82,7 @@ module CryptoExpectationsHelper
81
82
  decomposed_key: megar_test_sample_1.decomposed_key,
82
83
  size: downloader.download_size,
83
84
  iv: downloader.iv,
85
+ mac: downloader.mac,
84
86
  initial_counter_value: downloader.initial_counter_value
85
87
  }
86
88
  write_file_download_sample(megar_test_sample_1.name,downloader.raw_content)
@@ -92,6 +94,7 @@ module CryptoExpectationsHelper
92
94
  decomposed_key: megar_test_sample_2.decomposed_key,
93
95
  size: downloader.download_size,
94
96
  iv: downloader.iv,
97
+ mac: downloader.mac,
95
98
  initial_counter_value: downloader.initial_counter_value
96
99
  }
97
100
  write_file_download_sample(megar_test_sample_2.name,downloader.raw_content)
@@ -5,26 +5,20 @@ describe Megar::FileDownloader do
5
5
  let(:instance) { model_class.new(attributes) }
6
6
  let(:attributes) { {} }
7
7
 
8
- describe "#get_chunks (protected)" do
9
- subject { instance.send(:get_chunks,size) }
10
- {
11
- 122000 => [[0, 122000]],
12
- 332000 => [[0, 131072], [131072, 200928]],
13
- 500000 => [[0, 131072], [131072, 262144], [393216, 106784]],
14
- 800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 13568]],
15
- 1800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 489280]],
16
- 2000000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 33920]],
17
- 2800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 47488]],
18
- 3800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 129984]],
19
- 4800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 1048576], [4718592, 81408]],
20
- 20800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 1048576], [4718592, 1048576], [5767168, 1048576], [6815744, 1048576], [7864320, 1048576], [8912896, 1048576], [9961472, 1048576], [11010048, 1048576], [12058624, 1048576], [13107200, 1048576], [14155776, 1048576], [15204352, 1048576], [16252928, 1048576], [17301504, 1048576], [18350080, 1048576], [19398656, 1048576], [20447232, 352768]],
21
- }.each do |size,chunks|
22
- context "when size=#{size}" do
23
- let(:size) { size }
24
- let(:expected) { chunks }
25
- it { should eql(chunks) }
8
+ describe "#mac" do
9
+ subject { instance.mac }
10
+ [
11
+ {
12
+ file_key: [1029178532, 1095006796,361733076,-1656803926,1405858242,1396716347,870048465,-473559286],
13
+ expected_mac: [870048465,-473559286]
14
+ }
15
+ ].each do |options|
16
+ context "when file key #{options[:file_key]}" do
17
+ before { instance.stub(:key).and_return(options[:file_key]) }
18
+ it { should eql(options[:expected_mac]) }
26
19
  end
27
20
  end
21
+
28
22
  end
29
23
 
30
24
  crypto_expectations['sample_files'].keys.each do |sample_file_name|
@@ -78,6 +72,12 @@ describe Megar::FileDownloader do
78
72
  it { should eql(expected) }
79
73
  end
80
74
 
75
+ describe "#mac" do
76
+ let(:expected) { file_expectation['mac'] }
77
+ subject { instance.mac }
78
+ it { should eql(expected) }
79
+ end
80
+
81
81
  describe "#initial_counter_value" do
82
82
  let(:expected) { file_expectation['initial_counter_value'] }
83
83
  subject { instance.initial_counter_value }
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Megar::FileUploader do
4
+ let(:model_class) { Megar::FileUploader }
5
+ let(:instance) { model_class.new(attributes) }
6
+ let(:attributes) { {} }
7
+
8
+ subject { instance }
9
+
10
+ describe "#upload_key" do
11
+ subject { instance.upload_key }
12
+ it { should be_a(Array) }
13
+ its(:size) { should eql(6) }
14
+ end
15
+
16
+ describe "keys" do
17
+ subject { instance }
18
+ [
19
+ {
20
+ upload_key: [3230625094, 2656764682, 1008836587, 1082599785, 1919494632, 3993726968],
21
+ expected_iv: 152078032723025278718811426614033776640,
22
+ expected_mac_iv: [1919494632, 3993726968,1919494632, 3993726968],
23
+ expected_mac_encryption_key: [3230625094, 2656764682, 1008836587, 1082599785]
24
+ }
25
+ ].each do |options|
26
+ context "when upload_key #{options[:upload_key]}" do
27
+ before { instance.stub(:upload_key).and_return(options[:upload_key]) }
28
+ its(:iv) { should eql(options[:expected_iv]) }
29
+ its(:mac_iv) { should eql(options[:expected_mac_iv]) }
30
+ its(:mac_encryption_key) { should eql(options[:expected_mac_encryption_key]) }
31
+ end
32
+ end
33
+ end
34
+
35
+ context "with sample file" do
36
+ let(:file_name) { 'megar_test_sample_1.txt' }
37
+ let(:file_expectation) { crypto_expectations['sample_files'][file_name] }
38
+ let(:attributes) { { body: file_handle } }
39
+
40
+ context "when given a File" do
41
+ let(:file_handle) { File.open(crypto_sample_file_path(file_name),'rb') }
42
+ it { file_handle.should be_a(File) }
43
+ its(:upload_size) { should eql(file_expectation['size']) }
44
+ its(:name) { should eql(file_name) }
45
+
46
+ context "when override the filename" do
47
+ let(:new_name) { 'a new name.txt' }
48
+ let(:attributes) { { body: file_handle, name: new_name } }
49
+ its(:name) { should eql(new_name) }
50
+ end
51
+ end
52
+
53
+ context "when given a Pathname" do
54
+ let(:file_handle) { crypto_sample_file_path(file_name) }
55
+ it { file_handle.should be_a(Pathname) }
56
+ describe "#upload_size" do
57
+ subject { instance.upload_size }
58
+ it { should eql(file_expectation['size']) }
59
+ end
60
+ end
61
+
62
+ context "when given a file path as a string" do
63
+ let(:file_handle) { crypto_sample_file_path(file_name).to_s }
64
+ it { file_handle.should be_a(String) }
65
+ describe "#upload_size" do
66
+ subject { instance.upload_size }
67
+ it { should eql(file_expectation['size']) }
68
+ end
69
+ end
70
+
71
+ context "when given a path string to a non-existent file" do
72
+ let(:file_handle) { crypto_sample_file_path(file_name).to_s + '.bogative' }
73
+ it "should raise an error" do
74
+ expect { instance }.to raise_error(Errno::ENOENT)
75
+ end
76
+ end
77
+
78
+ describe "#post!" do
79
+ let(:file_handle) { crypto_sample_file_path(file_name) }
80
+ let(:session) { connected_session_with_mocked_api_responses }
81
+ let(:uploader) { Megar::FileUploader.new(attributes.merge(folder: session.folders.root)) }
82
+ let(:do_post) { uploader.post! }
83
+ let(:upload_attributes_response) { {"f"=>[session.files.find_by_name(file_name).payload]} }
84
+ let(:completion_file_handle) { "h4Zgt7cRyMUlzl6WguZALImGOr2Yyr31cTXd" }
85
+
86
+ it "should upload in a single chunk" do
87
+ uploader.should_receive(:post_chunk).and_return(completion_file_handle)
88
+ uploader.should_receive(:send_file_upload_attributes).and_return(upload_attributes_response)
89
+ do_post
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+
96
+ end
@@ -23,4 +23,16 @@ describe Megar::Files do
23
23
  end
24
24
  end
25
25
 
26
+ describe "#create" do
27
+ let(:attributes) { { name: 'a name', body: 'file_handle'} }
28
+ subject { instance.create(attributes) }
29
+ it "should create a valid uploader and post!" do
30
+ instance.stub(:parent_folder).and_return('parent_folder')
31
+ uploader = mock()
32
+ uploader.should_receive(:post!)
33
+ Megar::FileUploader.should_receive(:new).with(attributes.merge(folder: 'parent_folder')).and_return(uploader)
34
+ subject
35
+ end
36
+ end
37
+
26
38
  end
@@ -44,6 +44,16 @@ describe Megar::Folder do
44
44
  its(:id) { should eql('dir3') }
45
45
  its(:parent_folder) { should eql(folder) }
46
46
  end
47
+ describe "#create" do
48
+ let(:attributes) { { name: 'a name', body: 'file_handle'} }
49
+ subject { folder.create(attributes) }
50
+ it "should create a valid uploader and post!" do
51
+ uploader = mock()
52
+ uploader.should_receive(:post!)
53
+ Megar::FileUploader.should_receive(:new).with(attributes.merge(folder: folder)).and_return(uploader)
54
+ subject
55
+ end
56
+ end
47
57
  end
48
58
 
49
59
  context "when child folders not present" do
@@ -18,6 +18,7 @@ describe Megar::CryptoSupport do
18
18
  subject { harness.str_to_a32(string) }
19
19
  # expectation generation in Javascript:
20
20
  # str_to_a32(base64urldecode('zL-S9BspoEopTUm3z3O8CA'))
21
+ # str_to_a32(base64urldecode('YmxlLi4AAAAAAAAAAAAAAA'))
21
22
  [
22
23
  { given: "\xCC\xBF\x92\xF4\e)\xA0J)MI\xB7\xCFs\xBC\b", expect: [-859860236,455712842,692930999,-814498808] },
23
24
  { given: 'a', expect: [1627389952] },
@@ -28,6 +29,15 @@ describe Megar::CryptoSupport do
28
29
  it { should eql(test_case[:expect]) }
29
30
  end
30
31
  end
32
+ [
33
+ { given: 'zL-S9BspoEopTUm3z3O8CA', expect: [-859860236,455712842,692930999,-814498808] },
34
+ { given: 'YmxlLi4AAAAAAAAAAAAAAA', expect: [1651270958, 771751936, 0, 0] }
35
+ ].each do |test_case|
36
+ context "given #{test_case[:given]}" do
37
+ let(:string) { harness.base64urldecode(test_case[:given]) }
38
+ it { should eql(test_case[:expect]) }
39
+ end
40
+ end
31
41
  end
32
42
 
33
43
  describe "#a32_to_str" do
@@ -500,4 +510,71 @@ describe Megar::CryptoSupport do
500
510
  end
501
511
  end
502
512
 
513
+ describe "#get_chunks" do
514
+ subject { harness.get_chunks(size) }
515
+ {
516
+ 122000 => [[0, 122000]],
517
+ 332000 => [[0, 131072], [131072, 200928]],
518
+ 500000 => [[0, 131072], [131072, 262144], [393216, 106784]],
519
+ 800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 13568]],
520
+ 1800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 489280]],
521
+ 2000000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 33920]],
522
+ 2800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 47488]],
523
+ 3800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 129984]],
524
+ 4800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 1048576], [4718592, 81408]],
525
+ 20800000 => [[0, 131072], [131072, 262144], [393216, 393216], [786432, 524288], [1310720, 655360], [1966080, 786432], [2752512, 917504], [3670016, 1048576], [4718592, 1048576], [5767168, 1048576], [6815744, 1048576], [7864320, 1048576], [8912896, 1048576], [9961472, 1048576], [11010048, 1048576], [12058624, 1048576], [13107200, 1048576], [14155776, 1048576], [15204352, 1048576], [16252928, 1048576], [17301504, 1048576], [18350080, 1048576], [19398656, 1048576], [20447232, 352768]],
526
+ }.each do |size,chunks|
527
+ context "when size=#{size}" do
528
+ let(:size) { size }
529
+ let(:expected) { chunks }
530
+ it { should eql(chunks) }
531
+ end
532
+ end
533
+ end
534
+
535
+ describe "#calculate_chunk_mac" do
536
+ subject { harness.calculate_chunk_mac(chunk,decomposed_key,iv) }
537
+ [
538
+ {
539
+ chunk_b64: 'Re_JkMdeElC-EdjpC0Aoxw9k6mymXoJq5Deqgx9a2Vpj8sX6l34B',
540
+ decomposed_key: [1455434630,1271130048,979342435,1808341711],
541
+ chunk_mac_iv: [758940180,1555777008,758940180,1555777008],
542
+ expected_mac: [2029949810, 584234195, 3282227752, 2170965113]
543
+ },{
544
+ chunk_b64: 'SnVzdCBhYm91dCB0aGUgc2ltcGxlc3QgZmlsZSBwb3NzaWJsZS4uSnVzdCBhYm91dCB0aGUgc2ltcGxlc3QgZmlsZSBwb3NzaWJsZS4uSnVzdCBhYm91dCB0aGUgc2ltcGxlc3QgZmlsZSBwb3NzaWJsZS4u',
545
+ decomposed_key: [2016295139, 2872496308, 1974113602, 40814121],
546
+ chunk_mac_iv: [3326582948, 288270468, 3326582948, 288270468],
547
+ expected_mac: [4085249905, 2687924527, 2252792538, 707619224]
548
+ }
549
+ ].each do |options|
550
+ context "when chunk_b64 = #{options[:chunk_b64]}" do
551
+ let(:chunk) { harness.base64urldecode(options[:chunk_b64]) }
552
+ let(:decomposed_key) { options[:decomposed_key] }
553
+ let(:iv) { options[:chunk_mac_iv] }
554
+ it { should eql(options[:expected_mac]) }
555
+ end
556
+ end
557
+ end
558
+
559
+ describe "#accumulate_mac" do
560
+ subject { harness.accumulate_mac(chunk,progressive_mac,key,iv) }
561
+ [
562
+ {
563
+ chunk_b64: 'Re_JkMdeElC-EdjpC0Aoxw9k6mymXoJq5Deqgx9a2Vpj8sX6l34B',
564
+ key: [1455434630, 1271130048, 979342435, 1808341711],
565
+ progressive_mac: [1649900877, 3786760977, 26559147, 602156686],
566
+ chunk_mac_iv: [758940180, 1555777008, 758940180, 1555777008],
567
+ expected_mac: [-259790568, 215606833, 1883564717, -1389129415]
568
+ }
569
+ ].each do |options|
570
+ context "when chunk_b64 = #{options[:chunk_b64]}" do
571
+ let(:chunk) { harness.base64urldecode(options[:chunk_b64]) }
572
+ let(:key) { options[:key] }
573
+ let(:progressive_mac) { options[:progressive_mac] }
574
+ let(:iv) { options[:chunk_mac_iv] }
575
+ it { should eql(options[:expected_mac]) }
576
+ end
577
+ end
578
+ end
579
+
503
580
  end
@@ -2,19 +2,18 @@ require 'spec_helper'
2
2
 
3
3
  describe "Megar Exceptions" do
4
4
 
5
- describe "Megar::Error" do
6
- let(:exception_class) { Megar::Error }
7
- subject { raise exception_class.new("test") }
8
- it "should raise correctly" do
9
- expect { subject }.to raise_error(exception_class)
10
- end
11
- end
12
-
13
- describe "Megar::CryptoSupportRequirementsError" do
14
- let(:exception_class) { Megar::CryptoSupportRequirementsError }
15
- subject { raise exception_class.new("test") }
16
- it "should raise correctly" do
17
- expect { subject }.to raise_error(exception_class)
5
+ [
6
+ Megar::Error,
7
+ Megar::CryptoSupportRequirementsError,
8
+ Megar::MacVerificationError,
9
+ Megar::UnsupportedFileHandleTypeError,
10
+ Megar::FileUploadError
11
+ ].each do |exception_class|
12
+ describe exception_class do
13
+ subject { raise exception_class.new("test") }
14
+ it "should raise correctly" do
15
+ expect { subject }.to raise_error(exception_class)
16
+ end
18
17
  end
19
18
  end
20
19
 
@@ -43,4 +43,16 @@ describe Megar::Shell do
43
43
  end
44
44
  end
45
45
 
46
+ describe "#put" do
47
+ let(:options) { ['-e=email','-p=pwd'] }
48
+ let(:argv) { ['put', 'file_name'] }
49
+ it "should invoke put" do
50
+ mock_session = mock()
51
+ mock_session.stub(:connected?).and_return(true)
52
+ shell.stub(:session).and_return(mock_session)
53
+ shell.should_receive(:put).with(argv.drop(1))
54
+ shell.run
55
+ end
56
+ end
57
+
46
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: megar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-02 00:00:00.000000000 Z
12
+ date: 2013-03-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: getoptions
@@ -172,6 +172,7 @@ files:
172
172
  - lib/megar.rb
173
173
  - lib/megar/adapters.rb
174
174
  - lib/megar/adapters/file_downloader.rb
175
+ - lib/megar/adapters/file_uploader.rb
175
176
  - lib/megar/catalog.rb
176
177
  - lib/megar/catalog/catalog_item.rb
177
178
  - lib/megar/catalog/file.rb
@@ -195,6 +196,7 @@ files:
195
196
  - spec/support/crypto_expectations_helper.rb
196
197
  - spec/support/mocks_helper.rb
197
198
  - spec/unit/adapters/file_downloader_spec.rb
199
+ - spec/unit/adapters/file_uploader_spec.rb
198
200
  - spec/unit/catalog/file_spec.rb
199
201
  - spec/unit/catalog/files_spec.rb
200
202
  - spec/unit/catalog/folder_spec.rb
@@ -239,6 +241,7 @@ test_files:
239
241
  - spec/support/crypto_expectations_helper.rb
240
242
  - spec/support/mocks_helper.rb
241
243
  - spec/unit/adapters/file_downloader_spec.rb
244
+ - spec/unit/adapters/file_uploader_spec.rb
242
245
  - spec/unit/catalog/file_spec.rb
243
246
  - spec/unit/catalog/files_spec.rb
244
247
  - spec/unit/catalog/folder_spec.rb