megar 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|