megar 0.0.1
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/.gitignore +19 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +11 -0
- data/CHANGELOG +5 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.rdoc +218 -0
- data/Rakefile +33 -0
- data/bin/megar +16 -0
- data/lib/extensions/math.rb +13 -0
- data/lib/js_ref_impl/_README +9 -0
- data/lib/js_ref_impl/base64_1.js +83 -0
- data/lib/js_ref_impl/crypto_5.js +1795 -0
- data/lib/js_ref_impl/download_8.js +867 -0
- data/lib/js_ref_impl/hex_1.js +76 -0
- data/lib/js_ref_impl/index_9.js +666 -0
- data/lib/js_ref_impl/js.manifest +115 -0
- data/lib/js_ref_impl/rsa_1.js +456 -0
- data/lib/js_ref_impl/sjcl_1.js +1 -0
- data/lib/js_ref_impl/upload_10.js +691 -0
- data/lib/megar.rb +11 -0
- data/lib/megar/catalog.rb +5 -0
- data/lib/megar/catalog/catalog_item.rb +90 -0
- data/lib/megar/catalog/file.rb +14 -0
- data/lib/megar/catalog/files.rb +13 -0
- data/lib/megar/catalog/folder.rb +20 -0
- data/lib/megar/catalog/folders.rb +28 -0
- data/lib/megar/connection.rb +84 -0
- data/lib/megar/crypto.rb +399 -0
- data/lib/megar/exception.rb +55 -0
- data/lib/megar/session.rb +157 -0
- data/lib/megar/shell.rb +87 -0
- data/lib/megar/version.rb +3 -0
- data/megar.gemspec +30 -0
- data/spec/fixtures/crypto_expectations/sample_user.json +109 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/crypto_expectations_helper.rb +44 -0
- data/spec/support/mocks_helper.rb +22 -0
- data/spec/unit/catalog/file_spec.rb +31 -0
- data/spec/unit/catalog/files_spec.rb +26 -0
- data/spec/unit/catalog/folder_spec.rb +28 -0
- data/spec/unit/catalog/folders_spec.rb +49 -0
- data/spec/unit/connection_spec.rb +50 -0
- data/spec/unit/crypto_spec.rb +476 -0
- data/spec/unit/exception_spec.rb +35 -0
- data/spec/unit/extensions/math_spec.rb +21 -0
- data/spec/unit/session_spec.rb +146 -0
- data/spec/unit/shell_spec.rb +18 -0
- metadata +238 -0
data/lib/megar.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/hash_with_indifferent_access'
|
3
|
+
require 'extensions/math'
|
4
|
+
|
5
|
+
require 'megar/version'
|
6
|
+
require 'megar/exception'
|
7
|
+
require 'megar/shell'
|
8
|
+
require 'megar/crypto'
|
9
|
+
require 'megar/connection'
|
10
|
+
require 'megar/session'
|
11
|
+
require 'megar/catalog'
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# This module defines the basic naming interface for catalog objects
|
2
|
+
# Override these methods as required
|
3
|
+
module Megar::CatalogItem
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Adds an item to the local cached collection given +attributes+ hash:
|
7
|
+
# id: id / mega node handle
|
8
|
+
# payload: the literal mega node descriptor
|
9
|
+
# type: the folder type
|
10
|
+
# key: the decrypted folder key
|
11
|
+
# attributes: the decrypted attributes collection
|
12
|
+
def initialize(attributes={})
|
13
|
+
self.attributes = attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
# The ID (Mega handle)
|
17
|
+
attr_accessor :id
|
18
|
+
|
19
|
+
# The folder name
|
20
|
+
attr_accessor :name
|
21
|
+
alias_method :n=, :name=
|
22
|
+
|
23
|
+
# The literal mega node descriptor (as received from API)
|
24
|
+
attr_accessor :payload
|
25
|
+
|
26
|
+
# Assigns the payload attribute, also splitting out separate attribute assignments from +value+ if a hash
|
27
|
+
def payload=(value)
|
28
|
+
self.attributes = value
|
29
|
+
@payload = value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Assigns the attribute values splitting out separate attribute assignments from +value+ if a hash
|
33
|
+
def attributes=(value)
|
34
|
+
return unless value.respond_to?(:keys)
|
35
|
+
value.keys.each do |key|
|
36
|
+
if respond_to?(assignment = "#{key}=".to_sym)
|
37
|
+
send(assignment,value[key])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# The decrypted node key
|
43
|
+
attr_accessor :key
|
44
|
+
|
45
|
+
# The folder type id
|
46
|
+
# 0: File
|
47
|
+
# 1: Directory
|
48
|
+
# 2: Special node: Root (“Cloud Drive”)
|
49
|
+
# 3: Special node: Inbox
|
50
|
+
# 4: Special node: Trash Bin
|
51
|
+
attr_accessor :type
|
52
|
+
|
53
|
+
|
54
|
+
# Generic interface to return the currently applicable collection
|
55
|
+
def collection
|
56
|
+
@collection ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the expected class of items in the collection
|
60
|
+
def resource_class
|
61
|
+
"#{self.class.name}".chomp('s').constantize
|
62
|
+
end
|
63
|
+
|
64
|
+
# Command: clears/re-initialises the collection
|
65
|
+
def reset!
|
66
|
+
@collection = []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Implements Enumerable#each
|
70
|
+
def each
|
71
|
+
collection.each { |item| yield item }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns indexed elements from the collection
|
75
|
+
def [](*args)
|
76
|
+
collection[*args]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Equality based on ID
|
80
|
+
def ==(other)
|
81
|
+
self.id == other.id
|
82
|
+
end
|
83
|
+
alias_method :eql?, :==
|
84
|
+
|
85
|
+
# Returns the first record matching +type+
|
86
|
+
def find_by_type(type)
|
87
|
+
find { |r| r.type == type }
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# File collection item
|
2
|
+
class Megar::File
|
3
|
+
include Megar::CatalogItem
|
4
|
+
|
5
|
+
# The file size
|
6
|
+
attr_accessor :size
|
7
|
+
alias_method :s=, :size=
|
8
|
+
|
9
|
+
# Return a pretty version of the file record
|
10
|
+
def to_s
|
11
|
+
format("%16d bytes %-10s %-60s", size.to_i, id, name)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Collection manager for files
|
2
|
+
class Megar::Files
|
3
|
+
include Megar::CatalogItem
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
end
|
7
|
+
|
8
|
+
# Adds an item to the local cached collection given +attributes+ hash.
|
9
|
+
def add(attributes)
|
10
|
+
collection << Megar::File.new(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Folder collection item
|
2
|
+
class Megar::Folder
|
3
|
+
include Megar::CatalogItem
|
4
|
+
|
5
|
+
# Name assignment with special-purpose name support
|
6
|
+
def name=(value)
|
7
|
+
@name = if "#{value}" != ""
|
8
|
+
value
|
9
|
+
elsif type.is_a?(Fixnum)
|
10
|
+
["Cloud Drive","Inbox","Trash Bin"][type-2]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override initialisation to set special-purpose names
|
15
|
+
def initialize(attributes={})
|
16
|
+
super
|
17
|
+
self.name = nil unless name
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Collection manager for folders
|
2
|
+
class Megar::Folders
|
3
|
+
include Megar::CatalogItem
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
end
|
7
|
+
|
8
|
+
# Adds an item to the local cached collection given +attributes+ hash.
|
9
|
+
def add(attributes)
|
10
|
+
collection << Megar::Folder.new(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the root (cloud drive) folder
|
14
|
+
def root
|
15
|
+
@root ||= find_by_type(2)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the inbox folder
|
19
|
+
def inbox
|
20
|
+
@inbox ||= find_by_type(3)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the trash folder
|
24
|
+
def trash
|
25
|
+
@trash ||= find_by_type(4)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# A simple module to encapsulate network interation
|
7
|
+
#
|
8
|
+
module Megar::Connection
|
9
|
+
|
10
|
+
attr_accessor :sid
|
11
|
+
|
12
|
+
# Returns the current session sequence_number.
|
13
|
+
# On first request, it is initialised to a random integer.
|
14
|
+
def sequence_number
|
15
|
+
@sequence_number ||= rand(0xFFFFFFFF)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set the secuence number to +value+ (Fixnum)
|
19
|
+
def sequence_number=(value)
|
20
|
+
@sequence_number = value
|
21
|
+
end
|
22
|
+
|
23
|
+
# Command: increments and returns the next sequence number
|
24
|
+
def next_sequence_number!
|
25
|
+
sequence_number && @sequence_number += 1
|
26
|
+
end
|
27
|
+
|
28
|
+
# There seem to be a number of regional API enpoints,
|
29
|
+
# but not sure if there is any guidance yet as to which you should use.
|
30
|
+
# Known endpoints: https://g.api.mega.co.nz/cs, https://eu.api.mega.co.nz/cs
|
31
|
+
DEFAULT_API_ENDPOINT = 'https://eu.api.mega.co.nz/cs'
|
32
|
+
|
33
|
+
# Return the API endpoint url (String) - defaults to DEFAULT_API_ENDPOINT
|
34
|
+
def api_endpoint
|
35
|
+
@api_endpoint ||= DEFAULT_API_ENDPOINT
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set the API endpoint url to +value+ (String)
|
39
|
+
def api_endpoint=(value)
|
40
|
+
@api_endpoint = value
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the API endpoint uri
|
44
|
+
def api_uri
|
45
|
+
@api_uri ||= URI.parse(api_endpoint)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Command: Perform a single API request given +data+
|
49
|
+
def api_request(data)
|
50
|
+
params = {'id' => next_sequence_number!}
|
51
|
+
params['sid'] = sid if sid
|
52
|
+
json_data = [data].to_json
|
53
|
+
|
54
|
+
response_data = get_api_response(params,json_data).first
|
55
|
+
|
56
|
+
raise Megar::MegaRequestError.new(response_data) if response_data.is_a?(Fixnum)
|
57
|
+
|
58
|
+
response_data
|
59
|
+
end
|
60
|
+
|
61
|
+
# Command: low-level method to actually perform the API request and return the JSON response.
|
62
|
+
# Given +params+ Hash of query string parameters, and +data+ JSON data structure.
|
63
|
+
# Note: there is no handling of network errors or timeouts - any exceptions will bubble up.
|
64
|
+
def get_api_response(params,data)
|
65
|
+
http = Net::HTTP.new(api_uri.host, api_uri.port)
|
66
|
+
http.use_ssl = (api_uri.scheme == 'https')
|
67
|
+
uri_path = api_uri.path.empty? ? '/' : api_uri.path
|
68
|
+
uri_path << hash_to_query_string(params)
|
69
|
+
response = http.post(uri_path,data)
|
70
|
+
JSON.parse(response.body)
|
71
|
+
end
|
72
|
+
protected :get_api_response
|
73
|
+
|
74
|
+
# Returns Hash +h+ as an encoded query string '?a=b&c=d...'
|
75
|
+
def hash_to_query_string(h)
|
76
|
+
if qs = URI.escape(h.to_a.map{|e| e.join('=') }.join('&'))
|
77
|
+
'?' + qs
|
78
|
+
else
|
79
|
+
''
|
80
|
+
end
|
81
|
+
end
|
82
|
+
protected :hash_to_query_string
|
83
|
+
|
84
|
+
end
|
data/lib/megar/crypto.rb
ADDED
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
# A straight-forward "quirks-mode" transcoding of core crypto methods required to talk to Mega.
|
5
|
+
# Some of this reeks a bit .. maybe more idiomatic ruby approaches are possible.
|
6
|
+
#
|
7
|
+
# Generally we're using signed 32-bit by default here ... I don't think it's necessary, but it makes comparison with
|
8
|
+
# the javascript implementation easier.
|
9
|
+
#
|
10
|
+
# Javascript reference implementations quoted here are taken from the Mega javascript source.
|
11
|
+
#
|
12
|
+
module Megar::Crypto
|
13
|
+
|
14
|
+
# Returns encrypted key given an array +a+ of 32-bit integers
|
15
|
+
#
|
16
|
+
# Javascript reference implementation: function prepare_key(a)
|
17
|
+
#
|
18
|
+
def prepare_key(a)
|
19
|
+
pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
|
20
|
+
0x10000.times do
|
21
|
+
(0..(a.length-1)).step(4) do |j|
|
22
|
+
key = [0,0,0,0]
|
23
|
+
4.times {|i| key[i] = a[i+j] if (i+j < a.length) }
|
24
|
+
pkey = aes_encrypt_a32(pkey,key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
pkey
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns encrypted key given the plain-text +password+ string
|
31
|
+
#
|
32
|
+
# Javascript reference implementation: function prepare_key_pw(password)
|
33
|
+
#
|
34
|
+
def prepare_key_pw(password)
|
35
|
+
prepare_key(str_to_a32(password))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a decrypted given an array +a+ of 32-bit integers and +key+
|
39
|
+
#
|
40
|
+
# Javascript reference implementation: function decrypt_key(cipher,a)
|
41
|
+
#
|
42
|
+
def decrypt_key(a, key)
|
43
|
+
b=[]
|
44
|
+
(0..(a.length-1)).step(4) do |i|
|
45
|
+
b.concat aes_cbc_decrypt_a32(a[i,4], key)
|
46
|
+
end
|
47
|
+
b
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns decrypted array of 32-bit integers representation of base64 +data+ decrypted using +key+
|
51
|
+
def decrypt_base64_to_a32(data,key)
|
52
|
+
decrypt_key(base64_to_a32(data), key)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns decrypted string representation of base64 +data+ decrypted using +key+
|
56
|
+
def decrypt_base64_to_str(data,key)
|
57
|
+
a32_to_str(decrypt_base64_to_a32(data, key))
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# Returns AES-128 encrypted given +key+ and +data+ (arrays of 32-bit signed integers)
|
62
|
+
def aes_encrypt_a32(data, key)
|
63
|
+
aes = OpenSSL::Cipher::Cipher.new('AES-128-ECB')
|
64
|
+
aes.encrypt
|
65
|
+
aes.padding = 0
|
66
|
+
aes.key = key.pack('l>*')
|
67
|
+
aes.update(data.pack('l>*')).unpack('l>*')
|
68
|
+
# e = aes.update(data.pack('l>*')).unpack('l>*')
|
69
|
+
# e << aes.final
|
70
|
+
# e.unpack('l>*')
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns AES-128 decrypted given +key+ and +data+ (arrays of 32-bit signed integers)
|
74
|
+
def aes_cbc_decrypt_a32(data, key)
|
75
|
+
str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns AES-128 decrypted given +key+ and +data+ (String)
|
79
|
+
def aes_cbc_decrypt(data, key)
|
80
|
+
aes = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
|
81
|
+
aes.decrypt
|
82
|
+
aes.padding = 0
|
83
|
+
aes.key = key
|
84
|
+
aes.iv = "\0" * 16
|
85
|
+
d = aes.update(data)
|
86
|
+
d = aes.final if d.empty?
|
87
|
+
d
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an array of 32-bit signed integers representing the string +b+
|
91
|
+
#
|
92
|
+
# Javascript reference implementation: function str_to_a32(b)
|
93
|
+
#
|
94
|
+
def str_to_a32(b)
|
95
|
+
a = Array.new((b.length+3) >> 2,0)
|
96
|
+
b.length.times { |i| a[i>>2] |= (b.getbyte(i) << (24-(i & 3)*8)) }
|
97
|
+
a.pack('l>*').unpack('l>*') # hack to force to signed 32-bit ... I don't think we really need to do this, but it makes comparison with
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a packed string given an array +a+ of 32-bit signed integers
|
101
|
+
#
|
102
|
+
# Javascript reference implementation: function a32_to_str(a)
|
103
|
+
#
|
104
|
+
def a32_to_str(a)
|
105
|
+
b = ''
|
106
|
+
(a.size * 4).times { |i| b << ((a[i>>2] >> (24-(i & 3)*8)) & 255).chr }
|
107
|
+
b
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns a base64-encoding of string +s+ hashed with +aeskey+ key
|
111
|
+
#
|
112
|
+
# Javascript reference implementation: function stringhash(s,aes)
|
113
|
+
#
|
114
|
+
def stringhash(s,aeskey)
|
115
|
+
s32 = str_to_a32(s)
|
116
|
+
h32 = [0,0,0,0]
|
117
|
+
s32.length.times {|i| h32[i&3] ^= s32[i] }
|
118
|
+
16384.times {|i| h32 = aes_encrypt_a32(h32, aeskey) }
|
119
|
+
a32_to_base64([h32[0],h32[2]])
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns a base64-encoding given an array +a+ of 32-bit integers
|
123
|
+
#
|
124
|
+
# Javascript reference implementation: function a32_to_base64(a)
|
125
|
+
#
|
126
|
+
def a32_to_base64(a)
|
127
|
+
base64urlencode(a32_to_str(a))
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns an array +a+ of 32-bit integers given a base64-encoded +b+ (String)
|
131
|
+
#
|
132
|
+
# Javascript reference implementation: function base64_to_a32(s)
|
133
|
+
#
|
134
|
+
def base64_to_a32(s)
|
135
|
+
str_to_a32(base64urldecode(s))
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a base64-encoding given +data+ (String).
|
139
|
+
#
|
140
|
+
# Javascript reference implementation: function base64urlencode(data)
|
141
|
+
#
|
142
|
+
def base64urlencode(data)
|
143
|
+
Base64.urlsafe_encode64(data).gsub(/=*$/,'')
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns a string given +data+ (base64-encoded String)
|
147
|
+
#
|
148
|
+
# Javascript reference implementation: function base64urldecode(data)
|
149
|
+
#
|
150
|
+
def base64urldecode(data)
|
151
|
+
Base64.urlsafe_decode64(data + '=' * ((4 - data.length % 4) % 4))
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns multiple precision integer (MPI) as an array of 32-bit unsigned integers decoded from raw string +s+
|
155
|
+
# This first 16-bits of the MPI is the MPI length in bits
|
156
|
+
#
|
157
|
+
# Javascript reference implementation: function mpi2b(s)
|
158
|
+
#
|
159
|
+
def mpi_to_a32(s)
|
160
|
+
bs=28
|
161
|
+
bx2=1<<bs
|
162
|
+
bm=bx2-1
|
163
|
+
|
164
|
+
bn=1
|
165
|
+
r=[0]
|
166
|
+
rn=0
|
167
|
+
sb=256
|
168
|
+
c = nil
|
169
|
+
sn=s.length
|
170
|
+
return 0 if(sn < 2)
|
171
|
+
|
172
|
+
len=(sn-2)*8
|
173
|
+
bits=s[0].ord*256+s[1].ord
|
174
|
+
|
175
|
+
return 0 if(bits > len || bits < len-8)
|
176
|
+
|
177
|
+
len.times do |n|
|
178
|
+
if ((sb<<=1) > 255)
|
179
|
+
sb=1
|
180
|
+
sn -= 1
|
181
|
+
c=s[sn].ord
|
182
|
+
end
|
183
|
+
if(bn > bm)
|
184
|
+
bn=1
|
185
|
+
rn += 1
|
186
|
+
r << 0
|
187
|
+
end
|
188
|
+
if(c & sb != 0)
|
189
|
+
r[rn]|=bn
|
190
|
+
end
|
191
|
+
bn<<=1
|
192
|
+
end
|
193
|
+
r
|
194
|
+
end
|
195
|
+
|
196
|
+
# Alternative mpi2b implementation; doesn't quite match the javascript implementation yet however
|
197
|
+
# def native_mpi_to_a32(s)
|
198
|
+
# len = s.length - 2
|
199
|
+
# short = len % 4
|
200
|
+
# base = len - short
|
201
|
+
# r = s[2,base].unpack('N*')
|
202
|
+
# case short
|
203
|
+
# when 1
|
204
|
+
# r.concat s[2+base,short].unpack('C*')
|
205
|
+
# when 2
|
206
|
+
# r.concat s[2+base,short].unpack('n*')
|
207
|
+
# when 3
|
208
|
+
# r.concat ("\0" + s[2+base,short]).unpack('N*')
|
209
|
+
# end
|
210
|
+
# r
|
211
|
+
# end
|
212
|
+
|
213
|
+
# Returns multiple precision integer (MPI) as an array of 32-bit signed integers decoded from base64 string +s+
|
214
|
+
#
|
215
|
+
def base64_mpi_to_a32(s)
|
216
|
+
mpi_to_a32(base64urldecode(s))
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns multiple precision integer (MPI) as a big integers decoded from base64 string +s+
|
220
|
+
#
|
221
|
+
def base64_mpi_to_bn(s)
|
222
|
+
data = base64urldecode(s)
|
223
|
+
len = ((data[0].ord * 256 + data[1].ord + 7) / 8) + 2
|
224
|
+
data[2,len+2].unpack('H*').first.to_i(16)
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# Returns the 4-part RSA key as 32-bit signed integers [d, p, q, u] given +key+ (String)
|
229
|
+
#
|
230
|
+
# result[0] = p: The first factor of n, the RSA modulus
|
231
|
+
# result[1] = q: The second factor of n
|
232
|
+
# result[2] = d: The private exponent.
|
233
|
+
# result[3] = u: The CRT coefficient, equals to (1/p) mod q.
|
234
|
+
#
|
235
|
+
# Javascript reference implementation: function api_getsid2(res,ctx)
|
236
|
+
#
|
237
|
+
def decompose_rsa_private_key_a32(key)
|
238
|
+
privk = key.dup
|
239
|
+
decomposed_key = []
|
240
|
+
# puts "decomp: privk.len:#{privk.length}"
|
241
|
+
4.times do
|
242
|
+
len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
|
243
|
+
privk_part = privk[0,len]
|
244
|
+
# puts "\nprivk_part #{base64urlencode(privk_part)}"
|
245
|
+
privk_part_a32 = mpi_to_a32(privk_part)
|
246
|
+
decomposed_key << privk_part_a32
|
247
|
+
# puts "decomp: len:#{len} privk_part_a32:#{privk_part_a32.length} first:#{privk_part_a32.first} last:#{privk_part_a32.last}"
|
248
|
+
privk.slice!(0,len)
|
249
|
+
end
|
250
|
+
decomposed_key
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the 4-part RSA key as array of big integers [d, p, q, u] given +key+ (String)
|
254
|
+
#
|
255
|
+
# result[0] = p: The first factor of n, the RSA modulus
|
256
|
+
# result[1] = q: The second factor of n
|
257
|
+
# result[2] = d: The private exponent.
|
258
|
+
# result[3] = u: The CRT coefficient, equals to (1/p) mod q.
|
259
|
+
#
|
260
|
+
# Javascript reference implementation: function api_getsid2(res,ctx)
|
261
|
+
#
|
262
|
+
def decompose_rsa_private_key(key)
|
263
|
+
privk = key.dup
|
264
|
+
decomposed_key = []
|
265
|
+
offset = 0
|
266
|
+
4.times do |i|
|
267
|
+
len = ((privk[0].ord * 256 + privk[1].ord + 7) / 8) + 2
|
268
|
+
privk_part = privk[0,len]
|
269
|
+
# puts "\nl: ", len
|
270
|
+
# puts "decrypted rsa part hex: \n", privk_part.unpack('H*').first
|
271
|
+
decomposed_key << privk_part[2,privk_part.length].unpack('H*').first.to_i(16)
|
272
|
+
privk.slice!(0,len)
|
273
|
+
end
|
274
|
+
decomposed_key
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns the decrypted session id given base64 MPI +csid+ and RSA +rsa_private_key+ as array of big integers [d, p, q, u]
|
278
|
+
#
|
279
|
+
# Javascript reference implementation: function api_getsid2(res,ctx)
|
280
|
+
#
|
281
|
+
def decrypt_session_id(csid,rsa_private_key)
|
282
|
+
csid_bn = base64_mpi_to_bn(csid)
|
283
|
+
sid_bn = rsa_decrypt(csid_bn,rsa_private_key)
|
284
|
+
sid_hs = sid_bn.to_s(16)
|
285
|
+
sid_hs = '0' + sid_hs if sid_hs.length % 2 > 0
|
286
|
+
sid = hexstr_to_bstr(sid_hs)[0,43]
|
287
|
+
base64urlencode(sid)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Returns the private key decryption of +m+ given +pqdu+ (array of integer cipher components).
|
291
|
+
# Computes m**d (mod n).
|
292
|
+
#
|
293
|
+
# This implementation uses a Pure Ruby implementation of RSA private_decrypt
|
294
|
+
#
|
295
|
+
# p: The first factor of n, the RSA modulus
|
296
|
+
# q: The second factor of n
|
297
|
+
# d: The private exponent.
|
298
|
+
# u: The CRT coefficient, equals to (1/p) mod q.
|
299
|
+
#
|
300
|
+
# n = pq
|
301
|
+
# n is used as the modulus for both the public and private keys. Its length, usually expressed in bits, is the key length.
|
302
|
+
#
|
303
|
+
# φ(n) = (p – 1)(q – 1), where φ is Euler's totient function.
|
304
|
+
#
|
305
|
+
# Choose an integer e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1; i.e., e and φ(n) are coprime.
|
306
|
+
# e is released as the public key exponent
|
307
|
+
#
|
308
|
+
# Determine d as d ≡ e−1 (mod φ(n)), i.e., d is the multiplicative inverse of e (modulo φ(n)).
|
309
|
+
# d is kept as the private key exponent.
|
310
|
+
#
|
311
|
+
# More info: http://en.wikipedia.org/wiki/RSA_(algorithm)#Operation
|
312
|
+
#
|
313
|
+
# Javascript reference implementation: function RSAdecrypt(m, d, p, q, u)
|
314
|
+
#
|
315
|
+
def rsa_decrypt(m, pqdu)
|
316
|
+
p, q, d, u = pqdu
|
317
|
+
if p && q && u
|
318
|
+
m1 = Math.powm(m, d % (p-1), p)
|
319
|
+
m2 = Math.powm(m, d % (q-1), q)
|
320
|
+
h = m2 - m1
|
321
|
+
h = h + q if h < 0
|
322
|
+
h = h*u % q
|
323
|
+
h*p+m1
|
324
|
+
else
|
325
|
+
Math.powm(m, d, p*q)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
# Returns the private key decryption of +m+ given +pqdu+ (array of integer cipher components)
|
331
|
+
# This implementation uses OpenSSL RSA public key feature.
|
332
|
+
#
|
333
|
+
# NB: can't get this to work exactly right with Mega yet
|
334
|
+
def openssl_rsa_decrypt(m, pqdu)
|
335
|
+
rsa = openssl_rsa_cipher(pqdu)
|
336
|
+
|
337
|
+
chunk_size = 256 # hmm. need to figure out how to calc for "data greater than mod len"
|
338
|
+
# number.size(self.n) - 1 : Return the maximum number of bits that can be handled by this key.
|
339
|
+
decrypt_texts = []
|
340
|
+
(0..m.length - 1).step(chunk_size) do |i|
|
341
|
+
pt_part = m[i,chunk_size]
|
342
|
+
decrypt_texts << rsa.private_decrypt(pt_part,3)
|
343
|
+
end
|
344
|
+
decrypt_texts.join
|
345
|
+
end
|
346
|
+
|
347
|
+
# Returns an OpenSSL RSA cipher object initialised with +pqdu+ (array of integer cipher components)
|
348
|
+
# p: The first factor of n, the RSA modulus
|
349
|
+
# q: The second factor of n
|
350
|
+
# d: The private exponent.
|
351
|
+
# u: The CRT coefficient, equals to (1/p) mod q.
|
352
|
+
#
|
353
|
+
# NB: this hacks the RSA object creation n a way that should work, but can't get this to work exactly right with Mega yet
|
354
|
+
def openssl_rsa_cipher(pqdu)
|
355
|
+
rsa = OpenSSL::PKey::RSA.new
|
356
|
+
p, q, d, u = pqdu
|
357
|
+
rsa.p, rsa.q, rsa.d = p, q, d
|
358
|
+
rsa.n = rsa.p * rsa.q
|
359
|
+
# # dmp1 = d mod (p-1)
|
360
|
+
# rsa.dmp1 = rsa.d % (rsa.p - 1)
|
361
|
+
# # dmq1 = d mod (q-1)
|
362
|
+
# rsa.dmq1 = rsa.d % (rsa.q - 1)
|
363
|
+
# # iqmp = q^-1 mod p?
|
364
|
+
# rsa.iqmp = (rsa.q ** -1) % rsa.p
|
365
|
+
# # ipmq = (rsa.p ** -1) % rsa.q
|
366
|
+
# ipmq = rsa.p ** -1 % rsa.q
|
367
|
+
rsa.e = 0 # 65537
|
368
|
+
rsa
|
369
|
+
end
|
370
|
+
|
371
|
+
# Returns a binary string given a string +h+ of hex digits
|
372
|
+
def hexstr_to_bstr(h)
|
373
|
+
bstr = ''
|
374
|
+
(0..h.length-1).step(2) {|n| bstr << h[n,2].to_i(16).chr }
|
375
|
+
bstr
|
376
|
+
end
|
377
|
+
|
378
|
+
def decrypt_file_key(f)
|
379
|
+
key = f['k'].split(':')[1]
|
380
|
+
decrypt_key(base64_to_a32(key), self.master_key)
|
381
|
+
end
|
382
|
+
|
383
|
+
def decrypt_file_attributes(f,key)
|
384
|
+
k = f['t'] == 0 ? decompose_file_key(key) : key
|
385
|
+
rstr = aes_cbc_decrypt(base64urldecode(f['a']), a32_to_str(k))
|
386
|
+
JSON.parse( rstr.gsub("\x0",'').gsub(/^.*{/,'{'))
|
387
|
+
end
|
388
|
+
|
389
|
+
def decompose_file_key(key)
|
390
|
+
[
|
391
|
+
key[0] ^ key[4],
|
392
|
+
key[1] ^ key[5],
|
393
|
+
key[2] ^ key[6],
|
394
|
+
key[3] ^ key[7]
|
395
|
+
]
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
end
|