rmega 0.1.7 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/TODO.md +3 -5
- data/bin/rmega-dl +47 -0
- data/bin/rmega-up +31 -0
- data/lib/rmega.rb +35 -3
- data/lib/rmega/api_response.rb +80 -0
- data/lib/rmega/cli.rb +121 -0
- data/lib/rmega/crypto.rb +20 -0
- data/lib/rmega/crypto/aes_cbc.rb +46 -0
- data/lib/rmega/crypto/aes_ctr.rb +15 -84
- data/lib/rmega/crypto/aes_ecb.rb +25 -0
- data/lib/rmega/crypto/rsa.rb +21 -12
- data/lib/rmega/errors.rb +3 -51
- data/lib/rmega/loggable.rb +0 -3
- data/lib/rmega/net.rb +56 -0
- data/lib/rmega/nodes/deletable.rb +0 -3
- data/lib/rmega/nodes/downloadable.rb +73 -30
- data/lib/rmega/nodes/expandable.rb +14 -10
- data/lib/rmega/nodes/factory.rb +30 -17
- data/lib/rmega/nodes/file.rb +0 -4
- data/lib/rmega/nodes/folder.rb +4 -14
- data/lib/rmega/nodes/inbox.rb +0 -2
- data/lib/rmega/nodes/node.rb +48 -25
- data/lib/rmega/nodes/node_key.rb +44 -0
- data/lib/rmega/nodes/root.rb +0 -4
- data/lib/rmega/nodes/trash.rb +0 -3
- data/lib/rmega/nodes/uploadable.rb +42 -33
- data/lib/rmega/not_inspectable.rb +10 -0
- data/lib/rmega/options.rb +22 -5
- data/lib/rmega/pool.rb +18 -7
- data/lib/rmega/progress.rb +53 -13
- data/lib/rmega/session.rb +125 -52
- data/lib/rmega/storage.rb +25 -21
- data/lib/rmega/utils.rb +23 -183
- data/lib/rmega/version.rb +2 -1
- data/rmega.gemspec +3 -5
- data/spec/integration/file_download_spec.rb +14 -32
- data/spec/integration/file_integrity_spec.rb +41 -0
- data/spec/integration/file_upload_spec.rb +11 -57
- data/spec/integration/folder_download_spec.rb +17 -0
- data/spec/integration/folder_operations_spec.rb +30 -30
- data/spec/integration/login_spec.rb +3 -3
- data/spec/integration/resume_download_spec.rb +53 -0
- data/spec/integration_spec_helper.rb +9 -4
- data/spec/rmega/lib/cli_spec.rb +12 -0
- data/spec/rmega/lib/session_spec.rb +31 -0
- data/spec/rmega/lib/storage_spec.rb +27 -0
- data/spec/rmega/lib/utils_spec.rb +16 -78
- data/spec/spec_helper.rb +1 -4
- metadata +30 -40
- data/lib/rmega/crypto/aes.rb +0 -35
- data/lib/rmega/crypto/crypto.rb +0 -107
- data/lib/rmega/crypto/rsa_mega.js +0 -455
- data/spec/rmega/lib/crypto/aes_spec.rb +0 -12
- data/spec/rmega/lib/crypto/crypto_spec.rb +0 -27
@@ -1,19 +1,23 @@
|
|
1
|
-
require 'rmega/utils'
|
2
|
-
require 'rmega/nodes/uploadable'
|
3
|
-
require 'rmega/crypto/crypto'
|
4
|
-
|
5
1
|
module Rmega
|
6
2
|
module Nodes
|
7
3
|
module Expandable
|
8
4
|
include Uploadable
|
9
5
|
|
10
6
|
def create_folder(name)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
node_key = NodeKey.random
|
8
|
+
|
9
|
+
# encrypt attributes
|
10
|
+
attributes_str = "MEGA"
|
11
|
+
attributes_str << {n: name.strip}.to_json
|
12
|
+
attributes_str << ("\x00" * (16 - (attributes_str.size % 16)))
|
13
|
+
encrypted_attributes = aes_cbc_encrypt(node_key.aes_key, attributes_str)
|
14
|
+
|
15
|
+
# Encrypt node key
|
16
|
+
encrypted_key = aes_ecb_encrypt(session.master_key, node_key.aes_key)
|
17
|
+
|
18
|
+
n = [{h: 'xxxxxxxx', t: 1, a: Utils.base64urlencode(encrypted_attributes), k: Utils.base64urlencode(encrypted_key)}]
|
19
|
+
data = session.request(a: 'p', t: handle, n: n)
|
20
|
+
return Folder.new(session, data['f'][0])
|
17
21
|
end
|
18
22
|
|
19
23
|
def upload_url(filesize)
|
data/lib/rmega/nodes/factory.rb
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
require 'rmega/nodes/uploadable'
|
2
|
+
require 'rmega/nodes/expandable'
|
3
|
+
require 'rmega/nodes/downloadable'
|
4
|
+
require 'rmega/nodes/deletable'
|
5
|
+
require 'rmega/nodes/traversable'
|
6
|
+
require 'rmega/nodes/node_key'
|
1
7
|
require 'rmega/nodes/node'
|
2
8
|
require 'rmega/nodes/file'
|
3
9
|
require 'rmega/nodes/folder'
|
@@ -10,31 +16,38 @@ module Rmega
|
|
10
16
|
module Factory
|
11
17
|
extend self
|
12
18
|
|
19
|
+
URL_REGEXP = /mega\..+\/\#([A-Z0-9\_\-\!\=]+)/i
|
20
|
+
|
21
|
+
FOLDER_URL_REGEXP = /mega\..+\/\#\F([A-Z0-9\_\-\!\=]+)/i
|
22
|
+
|
23
|
+
def url?(string)
|
24
|
+
string.to_s =~ URL_REGEXP
|
25
|
+
end
|
26
|
+
|
13
27
|
def build(session, data)
|
14
|
-
|
15
|
-
|
16
|
-
node_class.new(session, data)
|
28
|
+
type = Node::TYPES[data['t']].to_s
|
29
|
+
return Nodes.const_get(type.capitalize).new(session, data)
|
17
30
|
end
|
18
31
|
|
19
|
-
|
20
|
-
def build_from_url(session, url)
|
32
|
+
def build_from_url(url, session = Session.new)
|
21
33
|
public_handle, key = url.strip.split('!')[1, 2]
|
22
|
-
data = session.request(a: 'g', g: 1, p: public_handle)
|
23
34
|
|
24
|
-
|
25
|
-
end
|
35
|
+
raise "Invalid url or missing file key" unless key
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
node = if url =~ FOLDER_URL_REGEXP
|
38
|
+
nodes_data = session.request({a: 'f', c: 1, r: 1}, {n: public_handle})
|
39
|
+
session.master_key = Utils.base64urldecode(key)
|
40
|
+
session.storage.nodes = nodes_data['f'].map { |data| Nodes::Factory.build(session, data) }
|
41
|
+
session.storage.nodes[0]
|
42
|
+
else
|
43
|
+
data = session.request(a: 'g', g: 1, p: public_handle)
|
44
|
+
Nodes::File.new(session, data)
|
45
|
+
end
|
30
46
|
|
31
|
-
|
32
|
-
|
33
|
-
founded_type.first if founded_type
|
34
|
-
end
|
47
|
+
node.instance_variable_set('@public_handle', public_handle)
|
48
|
+
node.instance_variable_set('@public_url', url)
|
35
49
|
|
36
|
-
|
37
|
-
{file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
|
50
|
+
return node
|
38
51
|
end
|
39
52
|
end
|
40
53
|
end
|
data/lib/rmega/nodes/file.rb
CHANGED
data/lib/rmega/nodes/folder.rb
CHANGED
@@ -1,10 +1,3 @@
|
|
1
|
-
require 'rmega/crypto/crypto'
|
2
|
-
require 'rmega/utils'
|
3
|
-
require 'rmega/nodes/node'
|
4
|
-
require 'rmega/nodes/expandable'
|
5
|
-
require 'rmega/nodes/traversable'
|
6
|
-
require 'rmega/nodes/deletable'
|
7
|
-
|
8
1
|
module Rmega
|
9
2
|
module Nodes
|
10
3
|
class Folder < Node
|
@@ -13,14 +6,11 @@ module Rmega
|
|
13
6
|
include Deletable
|
14
7
|
|
15
8
|
def download(path)
|
9
|
+
path = ::File.join(path, self.name)
|
10
|
+
FileUtils.mkdir_p(path)
|
11
|
+
|
16
12
|
children.each do |node|
|
17
|
-
|
18
|
-
node.download path
|
19
|
-
elsif node.type == :folder
|
20
|
-
subfolder = ::File.expand_path ::File.join(path, node.name)
|
21
|
-
Dir.mkdir(subfolder) unless Dir.exists?(subfolder)
|
22
|
-
node.download subfolder
|
23
|
-
end
|
13
|
+
node.download(path)
|
24
14
|
end
|
25
15
|
|
26
16
|
nil
|
data/lib/rmega/nodes/inbox.rb
CHANGED
data/lib/rmega/nodes/node.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
|
-
require 'rmega/loggable'
|
2
|
-
require 'rmega/utils'
|
3
|
-
require 'rmega/crypto/crypto'
|
4
|
-
require 'rmega/nodes/traversable'
|
5
|
-
|
6
1
|
module Rmega
|
7
2
|
module Nodes
|
8
3
|
class Node
|
4
|
+
include NotInspectable
|
9
5
|
include Loggable
|
10
6
|
include Traversable
|
7
|
+
include Crypto
|
11
8
|
|
12
9
|
attr_reader :data, :session
|
13
10
|
|
14
|
-
delegate :
|
11
|
+
delegate :request, :shared_keys, :rsa_privk, :master_key, :storage, :to => :session
|
12
|
+
|
13
|
+
TYPES = {0 => :file, 1 => :folder, 2 => :root, 3 => :inbox, 4 => :trash}
|
15
14
|
|
16
15
|
def initialize(session, data)
|
17
16
|
@session = session
|
@@ -19,14 +18,7 @@ module Rmega
|
|
19
18
|
end
|
20
19
|
|
21
20
|
def public_url
|
22
|
-
@public_url ||=
|
23
|
-
b64_dec_key = Utils.a32_to_base64(decrypted_file_key[0..7])
|
24
|
-
"https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def public_url=(url)
|
29
|
-
@public_url = url
|
21
|
+
@public_url ||= "https://mega.co.nz/#!#{public_handle}!#{Utils.base64urlencode(decrypted_file_key)}"
|
30
22
|
end
|
31
23
|
|
32
24
|
def public_handle
|
@@ -51,13 +43,14 @@ module Rmega
|
|
51
43
|
pairs = data['k'].split('/')
|
52
44
|
pairs.inject({}) do |hash, pair|
|
53
45
|
h, k = pair.split(':')
|
54
|
-
hash[h] = k
|
46
|
+
hash[h] = Utils.base64urldecode(k)
|
55
47
|
hash
|
56
48
|
end
|
57
49
|
end
|
58
50
|
|
59
51
|
def file_key
|
60
|
-
file_keys.values.first
|
52
|
+
k = file_keys.values.first
|
53
|
+
return k ? k : nil
|
61
54
|
end
|
62
55
|
|
63
56
|
def shared_root?
|
@@ -73,37 +66,67 @@ module Rmega
|
|
73
66
|
return unless sk
|
74
67
|
|
75
68
|
shared_key = if sk.size > 22
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
# Decrypt sk
|
70
|
+
sk = Utils.base64_mpi_to_bn(sk)
|
71
|
+
sk = rsa_decrypt(sk, rsa_privk)
|
72
|
+
sk = sk.to_s(16)
|
73
|
+
sk = '0' + sk if sk.size % 2 > 0
|
74
|
+
Utils.hexstr_to_bstr(sk)[0..15]
|
79
75
|
else
|
80
|
-
|
76
|
+
aes_ecb_decrypt(master_key, Utils.base64urldecode(sk))
|
81
77
|
end
|
82
78
|
|
83
79
|
shared_keys[handle] = shared_key
|
84
80
|
[handle, shared_key]
|
85
81
|
end
|
86
82
|
|
83
|
+
def self.each_chunk(size, &block)
|
84
|
+
start, p = 0, 0
|
85
|
+
|
86
|
+
return if size <= 0
|
87
|
+
|
88
|
+
loop do
|
89
|
+
offset = p < 8 ? (131072 * (p += 1)) : 1048576
|
90
|
+
next_start = offset + start
|
91
|
+
|
92
|
+
if next_start >= size
|
93
|
+
yield(start, size - start)
|
94
|
+
break
|
95
|
+
else
|
96
|
+
yield(start, offset)
|
97
|
+
start = next_start
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def each_chunk(&block)
|
103
|
+
self.class.each_chunk(filesize, &block)
|
104
|
+
end
|
105
|
+
|
87
106
|
def decrypted_file_key
|
88
107
|
h, shared_key = *process_shared_key
|
89
108
|
|
90
109
|
if shared_key
|
91
|
-
|
110
|
+
aes_ecb_decrypt(shared_key, file_keys[h])
|
92
111
|
elsif file_key
|
93
|
-
|
112
|
+
aes_ecb_decrypt(master_key, file_key)
|
94
113
|
else
|
95
|
-
Utils.
|
114
|
+
Utils.base64urldecode(public_url.split('!').last)
|
96
115
|
end
|
97
116
|
end
|
98
117
|
|
99
118
|
def attributes
|
100
119
|
encrypted = data['a'] || data['at']
|
101
120
|
return if !encrypted or encrypted.empty?
|
102
|
-
|
121
|
+
node_key = NodeKey.load(decrypted_file_key)
|
122
|
+
encrypted = Utils.base64urldecode(encrypted)
|
123
|
+
encrypted.strip! if encrypted.size % 16 != 0 # Fix possible errors
|
124
|
+
json = aes_cbc_decrypt(node_key.aes_key, encrypted)
|
125
|
+
JSON.parse json.gsub(/^MEGA/, '').rstrip
|
103
126
|
end
|
104
127
|
|
105
128
|
def type
|
106
|
-
|
129
|
+
TYPES[data['t']]
|
107
130
|
end
|
108
131
|
end
|
109
132
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Nodes
|
3
|
+
|
4
|
+
# The key associated to a node. It can be 128 or 256 bits long,
|
5
|
+
# when is 256 bits long is composed by:
|
6
|
+
# * A 128 bit AES-128 key
|
7
|
+
# * The upper 64 bit of the counter start value (the lower 64 bit
|
8
|
+
# are starting at 0 and incrementing by 1 for each AES block of 16
|
9
|
+
# bytes)
|
10
|
+
# * A 64 bit meta-MAC of all chunk MACs
|
11
|
+
class NodeKey
|
12
|
+
attr_reader :aes_key, :ctr_nonce, :meta_mac
|
13
|
+
attr_accessor :meta_mac
|
14
|
+
|
15
|
+
def initialize(string)
|
16
|
+
@aes_key = string[0..15]
|
17
|
+
@ctr_nonce = string[16..23]
|
18
|
+
@meta_mac = string[24..31]
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate
|
22
|
+
self.class.compact("#{@aes_key}#{@ctr_nonce}#{@meta_mac}") + @ctr_nonce + @meta_mac
|
23
|
+
end
|
24
|
+
|
25
|
+
# note: folder key is 16 bytes long while file key is 32
|
26
|
+
def self.load(string)
|
27
|
+
new("#{compact(string)}#{string[16..-1]}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.compact(string)
|
31
|
+
if string.size > 16
|
32
|
+
bytes = string.bytes.to_a
|
33
|
+
return 16.times.inject([]) { |ary, i| ary[i] = bytes[i] ^ bytes[i+16]; ary }.map(&:chr).join
|
34
|
+
else
|
35
|
+
return string
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.random
|
40
|
+
new(OpenSSL::Random.random_bytes(16 + 8 + 0))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/rmega/nodes/root.rb
CHANGED
data/lib/rmega/nodes/trash.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
require 'rmega/utils'
|
2
|
-
require 'rmega/pool'
|
3
|
-
require 'rmega/progress'
|
4
|
-
|
5
1
|
module Rmega
|
6
2
|
module Nodes
|
7
3
|
module Uploadable
|
4
|
+
include Net
|
5
|
+
|
8
6
|
def upload_chunk(base_url, start, buffer)
|
9
7
|
size = buffer.length
|
10
8
|
stop = start + size - 1
|
11
9
|
url = "#{base_url}/#{start}-#{stop}"
|
12
10
|
|
13
|
-
|
11
|
+
survive do
|
12
|
+
response = http_post(url, buffer)
|
13
|
+
raise("Upload failed") if response.code.to_i != 200
|
14
|
+
return response.body
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
def read_chunk(file, start, size)
|
@@ -18,65 +20,72 @@ module Rmega
|
|
18
20
|
file.read(size)
|
19
21
|
end
|
20
22
|
|
21
|
-
def encrypt_chunck(
|
22
|
-
|
23
|
-
|
24
|
-
encrypted = Crypto::AesCtr.encrypt(rnd_key[0..3], nonce, clean_buffer)
|
25
|
-
chunk_mac, data = encrypted[:mac], encrypted[:data]
|
26
|
-
|
27
|
-
file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
|
28
|
-
file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
|
23
|
+
def encrypt_chunck(start, clean_buffer, aes_key, nonce)
|
24
|
+
iv = nonce + [start/0x1000000000, start/0x10].pack('l>*')
|
25
|
+
enc_data = aes_ctr_encrypt(aes_key, clean_buffer, iv)
|
29
26
|
|
30
|
-
|
27
|
+
# calculate mac
|
28
|
+
mac_iv = nonce * 2
|
29
|
+
mac = aes_cbc_mac(aes_key, clean_buffer, mac_iv)
|
31
30
|
|
32
|
-
|
31
|
+
return [enc_data, mac]
|
33
32
|
end
|
34
33
|
|
35
34
|
def upload(path)
|
36
35
|
path = ::File.expand_path(path)
|
37
36
|
filesize = ::File.size(path)
|
37
|
+
|
38
|
+
raise "Empty file - #{path}" if filesize == 0
|
39
|
+
|
38
40
|
file = ::File.open(path, 'rb')
|
39
41
|
|
40
|
-
|
41
|
-
file_mac = [0, 0, 0, 0]
|
42
|
+
rnd_node_key = NodeKey.random
|
42
43
|
file_handle = nil
|
43
44
|
base_url = upload_url(filesize)
|
44
45
|
|
45
46
|
pool = Pool.new
|
46
47
|
read_mutex = Mutex.new
|
47
48
|
|
48
|
-
progress = Progress.new(
|
49
|
+
progress = Progress.new(filesize, caption: 'Upload')
|
50
|
+
|
51
|
+
chunk_macs = {}
|
49
52
|
|
50
|
-
|
51
|
-
pool.
|
52
|
-
|
53
|
+
self.class.each_chunk(filesize) do |start, size|
|
54
|
+
pool.process do
|
55
|
+
clean_buffer = nil
|
53
56
|
|
54
57
|
read_mutex.synchronize do
|
55
58
|
clean_buffer = read_chunk(file, start, size)
|
56
|
-
encrypted_buffer = encrypt_chunck(ul_key, file_mac, start, clean_buffer)
|
57
59
|
end
|
58
60
|
|
61
|
+
encrypted_buffer, chunk_mac = *encrypt_chunck(start, clean_buffer, rnd_node_key.aes_key, rnd_node_key.ctr_nonce)
|
59
62
|
file_handle = upload_chunk(base_url, start, encrypted_buffer)
|
63
|
+
chunk_macs[start] = chunk_mac
|
64
|
+
|
60
65
|
progress.increment(size)
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
64
|
-
pool.
|
65
|
-
|
66
|
-
attribs = {n: ::File.basename(path)}
|
67
|
-
encrypt_attribs = Utils.a32_to_base64(Crypto.encrypt_attributes(ul_key[0..3], attribs))
|
69
|
+
pool.shutdown
|
68
70
|
|
69
|
-
|
71
|
+
# encrypt attributes
|
72
|
+
attributes_str = "MEGA"
|
73
|
+
attributes_str << {n: ::File.basename(path)}.to_json
|
74
|
+
attributes_str << ("\x00" * (16 - (attributes_str.size % 16)))
|
75
|
+
encrypted_attributes = aes_cbc_encrypt(rnd_node_key.aes_key, attributes_str)
|
70
76
|
|
71
|
-
|
72
|
-
|
77
|
+
# Calculate meta_mac
|
78
|
+
file_mac = aes_cbc_mac(rnd_node_key.aes_key, chunk_macs.sort.map(&:last).join, "\x0"*16)
|
79
|
+
rnd_node_key.meta_mac = Utils.compact_to_8_bytes(file_mac)
|
80
|
+
encrypted_key = aes_ecb_encrypt(session.master_key, rnd_node_key.generate)
|
73
81
|
|
74
|
-
|
75
|
-
|
82
|
+
resp = request(a: 'p', t: handle, n: [
|
83
|
+
{h: file_handle, t: 0, a: Utils.base64urlencode(encrypted_attributes), k: Utils.base64urlencode(encrypted_key)}
|
84
|
+
])
|
76
85
|
|
77
|
-
|
86
|
+
return Nodes::Factory.build(session, resp['f'][0])
|
78
87
|
ensure
|
79
|
-
file.close
|
88
|
+
file.close if file
|
80
89
|
end
|
81
90
|
end
|
82
91
|
end
|