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 +4 -0
- data/README.rdoc +58 -7
- data/lib/megar/adapters.rb +2 -1
- data/lib/megar/adapters/file_downloader.rb +27 -43
- data/lib/megar/adapters/file_uploader.rb +148 -0
- data/lib/megar/catalog/catalog_item.rb +8 -1
- data/lib/megar/catalog/files.rb +22 -0
- data/lib/megar/catalog/folder.rb +17 -0
- data/lib/megar/crypto/support.rb +123 -5
- data/lib/megar/exception.rb +9 -0
- data/lib/megar/session.rb +51 -23
- data/lib/megar/shell.rb +15 -0
- data/lib/megar/version.rb +1 -1
- data/spec/fixtures/crypto_expectations/sample_user.json +29 -8
- data/spec/support/crypto_expectations_helper.rb +3 -0
- data/spec/unit/adapters/file_downloader_spec.rb +18 -18
- data/spec/unit/adapters/file_uploader_spec.rb +96 -0
- data/spec/unit/catalog/files_spec.rb +12 -0
- data/spec/unit/catalog/folder_spec.rb +10 -0
- data/spec/unit/crypto/support_spec.rb +77 -0
- data/spec/unit/exception_spec.rb +12 -13
- data/spec/unit/shell_spec.rb +12 -0
- metadata +5 -2
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
|
data/README.rdoc
CHANGED
@@ -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
|
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
|
-
|
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
|
data/lib/megar/adapters.rb
CHANGED
@@ -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
|
-
|
40
|
+
calculated_mac = accumulate_mac(decoded_chunk,calculated_mac,decomposed_key,chunk_mac_iv)
|
40
41
|
end
|
41
|
-
|
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,
|
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 ||=
|
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
|
-
|
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
|
|
data/lib/megar/catalog/files.rb
CHANGED
@@ -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
|
data/lib/megar/catalog/folder.rb
CHANGED
@@ -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
|
data/lib/megar/crypto/support.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/megar/exception.rb
CHANGED
@@ -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
|
data/lib/megar/session.rb
CHANGED
@@ -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
|
data/lib/megar/shell.rb
CHANGED
@@ -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
|
data/lib/megar/version.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"email": "megartest@gmail.com",
|
3
3
|
"email_mixed_case": "Megartest@gmail.com",
|
4
|
-
"password": "
|
4
|
+
"password": "NEvfEM5xME7jAnr6YN9JZH8M",
|
5
5
|
"autoconnect": false,
|
6
6
|
"login_response_data": {
|
7
|
-
"csid": "
|
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": "
|
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": "
|
18
|
-
"sid": "
|
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": "
|
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/
|
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/
|
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 "#
|
9
|
-
subject { instance.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
data/spec/unit/exception_spec.rb
CHANGED
@@ -2,19 +2,18 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe "Megar Exceptions" do
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
|
data/spec/unit/shell_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|