snxvpn 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/bin/snxvpn +4 -223
- data/lib/snxvpn.rb +17 -0
- data/lib/snxvpn/cli.rb +129 -0
- data/lib/snxvpn/config.rb +37 -0
- data/lib/snxvpn/ext_info.rb +28 -0
- data/lib/snxvpn/rsa.rb +57 -0
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5a236667c5ac56a6d62ce31f2b7542fa191bc02b5da9d128ce2ef34f960f936
|
4
|
+
data.tar.gz: 2d21393ea95878e147fd7f7716f791ae0987e9e3074589084440e8fabb980019
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d377b517ca04c53eaed0baa5d9109d36775e1bd6e907ae6c8fa38a59d208fcfbdbd4bf78cad840d4e003ec00755c78c122250ad1d96894db34311b8a35f58d4
|
7
|
+
data.tar.gz: 2f6f07413208739239ae042e3b25d0b5080b71cc6e5623b1ea5609dd91a612cd5994b63680310d041d83ad5808fc81161839c1af937bda57333be94310036bc7
|
data/bin/snxvpn
CHANGED
@@ -2,233 +2,14 @@
|
|
2
2
|
|
3
3
|
$stderr.sync = true
|
4
4
|
|
5
|
-
require '
|
6
|
-
require 'optparse'
|
7
|
-
require 'net/http'
|
8
|
-
require 'logger'
|
9
|
-
require 'openssl'
|
10
|
-
require 'base64'
|
11
|
-
require 'socket'
|
12
|
-
require 'ipaddr'
|
13
|
-
require 'open3'
|
14
|
-
|
15
|
-
class RetryableError < StandardError; end
|
16
|
-
|
17
|
-
class FunkyRSA < OpenSSL::PKey::RSA
|
18
|
-
|
19
|
-
def initialize(mod, exp)
|
20
|
-
pubkey = create_encoded_pubkey_from([mod].pack("H*"), [exp].pack("H*"))
|
21
|
-
hexkey = [pubkey].pack("H*")
|
22
|
-
pemkey = "-----BEGIN RSA PUBLIC KEY-----\n#{Base64.encode64 hexkey}-----END RSA PUBLIC KEY-----"
|
23
|
-
super(pemkey)
|
24
|
-
end
|
25
|
-
|
26
|
-
def hex_encrypt(s)
|
27
|
-
public_encrypt(s).reverse.unpack("H*").first
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def create_encoded_pubkey_from(rawmod, rawexp)
|
33
|
-
modulus_hex = prepad_signed(rawmod.unpack('H*').first)
|
34
|
-
exponent_hex = prepad_signed(rawexp.unpack('H*').first)
|
35
|
-
|
36
|
-
modlen = modulus_hex.length / 2
|
37
|
-
explen = exponent_hex.length / 2
|
38
|
-
encoded_modlen = encode_length_hex(modlen)
|
39
|
-
encoded_explen = encode_length_hex(explen)
|
40
|
-
|
41
|
-
full_hex_len = modlen + explen + encoded_modlen.length/2 + encoded_explen.length/2 + 2
|
42
|
-
encoded_hex_len = encode_length_hex(full_hex_len)
|
43
|
-
|
44
|
-
"30#{encoded_hex_len}02#{encoded_modlen}#{modulus_hex}02#{encoded_explen}#{exponent_hex}"
|
45
|
-
end
|
46
|
-
|
47
|
-
def prepad_signed(hex)
|
48
|
-
msb = hex[0]
|
49
|
-
(msb < '0' || msb > '7') ? "00#{hex}" : hex
|
50
|
-
end
|
51
|
-
|
52
|
-
def encode_length_hex(der_len) # encode ASN.1 DER length field
|
53
|
-
return int_to_hex(der_len) if der_len <= 127
|
54
|
-
|
55
|
-
hex = int_to_hex(der_len)
|
56
|
-
size = 128 + hex.length/2
|
57
|
-
"#{int_to_hex(size)}#{hex}"
|
58
|
-
end
|
59
|
-
|
60
|
-
def int_to_hex(num)
|
61
|
-
hex = num.to_s(16)
|
62
|
-
hex.length.odd? ? "0#{hex}" : hex
|
63
|
-
end
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
class Runner
|
68
|
-
ENTRY_PATH = '/SNX/Portal/Main'.freeze
|
69
|
-
DEFAULTS = {
|
70
|
-
'realm' => 'ssl_vpn',
|
71
|
-
'login_type' => 'Standard',
|
72
|
-
'username' => 'anonymous',
|
73
|
-
'password' => 'secret',
|
74
|
-
'snxpath' => 'snx',
|
75
|
-
}.freeze
|
76
|
-
|
77
|
-
def initialize(profile, config_path: File.join(ENV['HOME'], '.snxvpn'))
|
78
|
-
stat = File.stat(config_path)
|
79
|
-
raise ArgumentError, "#{config_path} is not owned by #{ENV['USER']}" unless stat.owned?
|
80
|
-
raise ArgumentError, "#{config_path} mode must be 600" if stat.world_readable? || stat.world_writable?
|
81
|
-
|
82
|
-
raw_config = YAML.load_file(config_path)
|
83
|
-
raise ArgumentError, "#{config_path} contains invalid config" unless raw_config.is_a?(Hash)
|
84
|
-
|
85
|
-
@config = if profile
|
86
|
-
raise ArgumentError, "Unable to find configuration for profile '#{profile}'" unless raw_config.key?(profile)
|
87
|
-
DEFAULTS.merge(raw_config[profile])
|
88
|
-
elsif raw_config.size == 1
|
89
|
-
DEFAULTS.merge(raw_config.values.first)
|
90
|
-
else
|
91
|
-
raise ArgumentError, 'No profile selected'
|
92
|
-
end
|
93
|
-
|
94
|
-
@http = Net::HTTP.new(@config['host'], 443)
|
95
|
-
@http.use_ssl = true
|
96
|
-
@cookies = ["selected_realm=#{@config['realm']}"]
|
97
|
-
end
|
98
|
-
|
99
|
-
def run(retries = 10)
|
100
|
-
# find RSA.js
|
101
|
-
resp = get(ENTRY_PATH)
|
102
|
-
rsa_path = resp.body.match(/<script .*?src *\= *["'](.*RSA.js)["']/).to_a[1]
|
103
|
-
raise RetryableError, "Unable to detect a RSA.js script reference on login page" unless rsa_path
|
104
|
-
|
105
|
-
# store paths
|
106
|
-
login_path = resp.uri
|
107
|
-
rsa_path = File.expand_path(rsa_path, File.dirname(login_path))
|
108
|
-
|
109
|
-
# fetch RSA.js and scan modulus and exponent
|
110
|
-
body = get(rsa_path).body
|
111
|
-
modulus = body.match(/var *modulus *\= *\'(\w+)\'/).to_a[1]
|
112
|
-
exponent = body.match(/var *exponent *\= *\'(\w+)\'/).to_a[1]
|
113
|
-
raise RetryableError "Unable to detect modulus/exponent in RSA.js script" unless modulus && exponent
|
114
|
-
|
115
|
-
# build RSA
|
116
|
-
rsa = FunkyRSA.new(modulus, exponent)
|
117
|
-
|
118
|
-
# post to login
|
119
|
-
resp = post(login_path,
|
120
|
-
password: rsa.hex_encrypt(@config['password']),
|
121
|
-
userName: @config['username'],
|
122
|
-
selectedRealm: @config['realm'],
|
123
|
-
loginType: @config['login_type'],
|
124
|
-
HeightData: '',
|
125
|
-
vpid_prefix: '',
|
126
|
-
)
|
127
|
-
raise RetryableError, "Expected redirect to multi-challenge, but got #{resp.uri}" unless resp.uri.include?('MultiChallenge')
|
128
|
-
|
129
|
-
# request OTP until successful
|
130
|
-
while resp.uri.include?('MultiChallenge')
|
131
|
-
inputs = resp.body.scan(/<input.*?type="hidden".*?name="(username|params|HeightData)"(?:.*?value="(.+?)")?/)
|
132
|
-
payload = Hash[inputs]
|
133
|
-
|
134
|
-
print " + Enter one-time password: "
|
135
|
-
otp = gets.strip
|
136
|
-
payload['password'] = rsa.hex_encrypt(otp)
|
137
|
-
resp = post(resp.uri, payload)
|
138
|
-
end
|
139
|
-
|
140
|
-
# request extender info
|
141
|
-
resp = get("/SNX/extender")
|
142
|
-
extinf = Hash[resp.body.scan(/Extender.(\w+) *= *"(.*?)" *;/)]
|
143
|
-
raise RetryableError, "Unable to retrieve extender information" if extinf.empty?
|
144
|
-
|
145
|
-
host_ip = IPAddr.new(IPSocket.getaddress(extinf['host_name']))
|
146
|
-
snxinf = [
|
147
|
-
"\x13\x11\x00\x00".encode('binary'), # 4-byte magic
|
148
|
-
[976].pack('L<'), # 4-byte data length
|
149
|
-
[host_ip.to_i].pack('L<'), # 4-byte host IP
|
150
|
-
extinf['host_name'].encode('binary').ljust(64, "\0"), # 64 bytes
|
151
|
-
[extinf['port'].to_i].pack('L<'), # 4-byte port
|
152
|
-
''.encode('binary').ljust(6, "\0"), # 6-bytes blank
|
153
|
-
extinf['server_cn'].encode('binary').ljust(256, "\0"), # 256 bytes
|
154
|
-
extinf['user_name'].encode('binary').ljust(256, "\0"), # 256 bytes
|
155
|
-
extinf['password'].encode('binary').ljust(128, "\0"), # 128 bytes
|
156
|
-
extinf['server_fingerprint'].encode('binary').ljust(256, "\0"), # 256 bytes
|
157
|
-
"\x01\x00".encode('binary'), # 2 bytes
|
158
|
-
].join
|
159
|
-
|
160
|
-
output, status = Open3.capture2('snx', '-Z')
|
161
|
-
raise RetryableError, "Unable to start snx: #{output}" unless status.success?
|
162
|
-
|
163
|
-
Socket.tcp("127.0.0.1", 7776) do |sock|
|
164
|
-
sock.write(snxinf)
|
165
|
-
sock.recv(4096) # read answer
|
166
|
-
puts " = Connected! Please leave this running to keep VPN open."
|
167
|
-
sock.recv(4096) # block until snx process dies
|
168
|
-
end
|
169
|
-
|
170
|
-
puts " ! Connection closed. Exiting..."
|
171
|
-
rescue RetryableError
|
172
|
-
raise if retries < 1
|
173
|
-
|
174
|
-
puts " ! #{e.message}. Retrying..."
|
175
|
-
sleep 1
|
176
|
-
run(retries - 1)
|
177
|
-
end
|
178
|
-
|
179
|
-
private
|
180
|
-
|
181
|
-
def post(path, payload)
|
182
|
-
resp = @http.post(path, URI.encode_www_form(payload), headers)
|
183
|
-
handle_response(path, resp)
|
184
|
-
|
185
|
-
case resp
|
186
|
-
when Net::HTTPSuccess then
|
187
|
-
resp
|
188
|
-
when Net::HTTPRedirection then
|
189
|
-
get(resp['location'])
|
190
|
-
else
|
191
|
-
raise "unexpected response from POST #{path} - #{resp.code} #{resp.message}"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
def get(path, retries = 10)
|
196
|
-
raise ArgumentError, 'too many HTTP redirects' if retries.zero?
|
197
|
-
|
198
|
-
resp = @http.get(path, headers)
|
199
|
-
handle_response(path, resp)
|
200
|
-
|
201
|
-
case resp
|
202
|
-
when Net::HTTPSuccess then
|
203
|
-
resp
|
204
|
-
when Net::HTTPRedirection then
|
205
|
-
get(resp['location'], retries - 1)
|
206
|
-
when Net::HTTPNotFound then
|
207
|
-
raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}" unless path.include?(ENTRY_PATH)
|
208
|
-
resp
|
209
|
-
else
|
210
|
-
raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}"
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def headers
|
215
|
-
{'Cookie' => @cookies.join('; ')} unless @cookies.empty?
|
216
|
-
end
|
217
|
-
|
218
|
-
def handle_response(path, resp)
|
219
|
-
resp.uri = path
|
220
|
-
@cookies += Array(resp.get_fields('set-cookie')).map do |str|
|
221
|
-
str.split('; ')[0]
|
222
|
-
end
|
223
|
-
@cookies.uniq!
|
224
|
-
end
|
225
|
-
|
226
|
-
end
|
5
|
+
require 'snxvpn'
|
227
6
|
|
228
7
|
begin
|
229
|
-
|
8
|
+
Snxvpn::CLI.new(ARGV[0]).run
|
230
9
|
rescue OpenSSL::SSL::SSLError => e
|
231
10
|
abort " ! #{e.message}. Are you already connected to the VPN?"
|
232
11
|
rescue StandardError => e
|
233
12
|
abort " ! #{e.message}. Exiting..."
|
13
|
+
rescue SignalException => e
|
14
|
+
abort " ! Received #{e.class}. Exiting..."
|
234
15
|
end if $0 == __FILE__
|
data/lib/snxvpn.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'optparse'
|
3
|
+
require 'net/http'
|
4
|
+
require 'logger'
|
5
|
+
require 'openssl'
|
6
|
+
require 'base64'
|
7
|
+
require 'socket'
|
8
|
+
require 'ipaddr'
|
9
|
+
require 'open3'
|
10
|
+
|
11
|
+
module Snxvpn
|
12
|
+
class RetryableError < ::StandardError; end
|
13
|
+
end
|
14
|
+
|
15
|
+
%w|config ext_info rsa cli|.each do |name|
|
16
|
+
require "snxvpn/#{name}"
|
17
|
+
end
|
data/lib/snxvpn/cli.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'optparse'
|
3
|
+
require 'net/http'
|
4
|
+
require 'logger'
|
5
|
+
require 'openssl'
|
6
|
+
require 'base64'
|
7
|
+
require 'socket'
|
8
|
+
require 'ipaddr'
|
9
|
+
require 'open3'
|
10
|
+
|
11
|
+
module Snxvpn
|
12
|
+
class CLI
|
13
|
+
|
14
|
+
attr_reader :config, :http
|
15
|
+
|
16
|
+
def initialize(profile, config_path: File.join(ENV['HOME'], '.snxvpn'))
|
17
|
+
@config = Config.new(config_path, profile)
|
18
|
+
@http = Net::HTTP.new(@config[:host], 443)
|
19
|
+
@http.use_ssl = true
|
20
|
+
@cookies = ["selected_realm=#{@config[:realm]}"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(retries = 10)
|
24
|
+
# find RSA.js
|
25
|
+
resp = get(config[:entry_path])
|
26
|
+
rsa_path = resp.body.match(/<script .*?src *\= *["'](.*RSA.js)["']/).to_a[1]
|
27
|
+
raise RetryableError, "Unable to detect a RSA.js script reference on login page" unless rsa_path
|
28
|
+
|
29
|
+
# store paths
|
30
|
+
login_path = resp.uri
|
31
|
+
rsa_path = File.expand_path(rsa_path, File.dirname(login_path))
|
32
|
+
|
33
|
+
# fetch RSA.js and parse RSA
|
34
|
+
rsa = RSA.parse(get(rsa_path).body)
|
35
|
+
raise RetryableError "Unable to detect modulus/exponent in RSA.js script" unless rsa
|
36
|
+
|
37
|
+
# post to login
|
38
|
+
resp = post(login_path,
|
39
|
+
password: rsa.hex_encrypt(config[:password]),
|
40
|
+
userName: config[:username],
|
41
|
+
selectedRealm: config[:realm],
|
42
|
+
loginType: config[:login_type],
|
43
|
+
HeightData: '',
|
44
|
+
vpid_prefix: '',
|
45
|
+
)
|
46
|
+
raise RetryableError, "Expected redirect to multi-challenge, but got #{resp.uri}" unless resp.uri.include?('MultiChallenge')
|
47
|
+
|
48
|
+
# request OTP until successful
|
49
|
+
inputs = resp.body.scan(/<input.*?type="hidden".*?name="(username|params|HeightData)"(?:.*?value="(.+?)")?/)
|
50
|
+
payload = Hash[inputs]
|
51
|
+
while resp.uri.include?('MultiChallenge')
|
52
|
+
print " + Enter one-time password: "
|
53
|
+
otp = gets.strip
|
54
|
+
payload['password'] = rsa.hex_encrypt(otp)
|
55
|
+
resp = post(resp.uri, payload)
|
56
|
+
end
|
57
|
+
|
58
|
+
# request extender info
|
59
|
+
ext_info = ExtInfo.new get("/SNX/extender").body
|
60
|
+
raise RetryableError, "Unable to retrieve extender information" if ext_info.empty?
|
61
|
+
|
62
|
+
output, status = Open3.capture2(config[:snx_path], '-Z')
|
63
|
+
raise RetryableError, "Unable to start snx: #{output}" unless status.success?
|
64
|
+
|
65
|
+
Socket.tcp('127.0.0.1', 7776) do |sock|
|
66
|
+
sock.write(ext_info.payload)
|
67
|
+
sock.recv(4096) # read answer
|
68
|
+
puts ' = Connected! Please leave this running to keep VPN open.'
|
69
|
+
sock.recv(4096) # block until snx process dies
|
70
|
+
end
|
71
|
+
|
72
|
+
puts ' ! Connection closed. Exiting...'
|
73
|
+
rescue RetryableError
|
74
|
+
raise if retries < 1
|
75
|
+
|
76
|
+
puts ' ! #{e.message}. Retrying...'
|
77
|
+
sleep 1
|
78
|
+
run(retries - 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def get(path, retries = 10)
|
84
|
+
raise ArgumentError, 'too many HTTP redirects' if retries.zero?
|
85
|
+
|
86
|
+
resp = @http.get(path, headers)
|
87
|
+
handle_response(path, resp)
|
88
|
+
|
89
|
+
case resp
|
90
|
+
when Net::HTTPSuccess then
|
91
|
+
resp
|
92
|
+
when Net::HTTPRedirection then
|
93
|
+
get(resp['location'], retries - 1)
|
94
|
+
when Net::HTTPNotFound then
|
95
|
+
raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}" unless path.include?(config[:entry_path])
|
96
|
+
resp
|
97
|
+
else
|
98
|
+
raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def post(path, payload)
|
103
|
+
resp = @http.post(path, URI.encode_www_form(payload), headers)
|
104
|
+
handle_response(path, resp)
|
105
|
+
|
106
|
+
case resp
|
107
|
+
when Net::HTTPSuccess then
|
108
|
+
resp
|
109
|
+
when Net::HTTPRedirection then
|
110
|
+
get(resp['location'])
|
111
|
+
else
|
112
|
+
raise "unexpected response from POST #{path} - #{resp.code} #{resp.message}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def headers
|
117
|
+
{'Cookie' => @cookies.join('; ')} unless @cookies.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_response(path, resp)
|
121
|
+
resp.uri = path
|
122
|
+
@cookies += Array(resp.get_fields('set-cookie')).map do |str|
|
123
|
+
str.split('; ')[0]
|
124
|
+
end
|
125
|
+
@cookies.uniq!
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Snxvpn
|
2
|
+
class Config < Hash
|
3
|
+
DEFAULTS = {
|
4
|
+
'realm' => 'ssl_vpn',
|
5
|
+
'login_type' => 'Standard',
|
6
|
+
'username' => 'anonymous',
|
7
|
+
'password' => 'secret',
|
8
|
+
'snx_path' => 'snx',
|
9
|
+
'entry_path' => '/SNX/Portal/Main',
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def initialize(path, profile = nil)
|
13
|
+
stat = File.stat(path)
|
14
|
+
|
15
|
+
raise ArgumentError, "#{path} is not owned by the current user" unless stat.owned?
|
16
|
+
raise ArgumentError, "#{path} mode must be 600" if stat.world_readable? || stat.world_writable?
|
17
|
+
|
18
|
+
raw = YAML.load_file(path)
|
19
|
+
raise ArgumentError, "#{path} contains invalid config" unless raw.is_a?(Hash)
|
20
|
+
|
21
|
+
update DEFAULTS
|
22
|
+
if profile
|
23
|
+
raise ArgumentError, "Unable to find configuration for profile '#{profile}'" unless raw.key?(profile)
|
24
|
+
update raw[profile]
|
25
|
+
elsif raw.size == 1
|
26
|
+
update raw.values.first
|
27
|
+
else
|
28
|
+
raise ArgumentError, 'No profile selected'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](key)
|
33
|
+
super(key.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Snxvpn
|
2
|
+
|
3
|
+
class ExtInfo < Hash
|
4
|
+
|
5
|
+
def initialize(body)
|
6
|
+
update Hash[body.scan(/Extender.(\w+) *= *"(.*?)" *;/)]
|
7
|
+
end
|
8
|
+
|
9
|
+
def payload
|
10
|
+
host_ip = IPAddr.new(IPSocket.getaddress(self['host_name']))
|
11
|
+
snxinf = [
|
12
|
+
"\x13\x11\x00\x00".encode('binary'), # 4-byte magic
|
13
|
+
[976].pack('L<'), # 4-byte data length
|
14
|
+
[host_ip.to_i].pack('L<'), # 4-byte host IP
|
15
|
+
self['host_name'].encode('binary').ljust(64, "\0"), # 64 bytes
|
16
|
+
[self['port'].to_i].pack('L<'), # 4-byte port
|
17
|
+
''.encode('binary').ljust(6, "\0"), # 6-bytes blank
|
18
|
+
self['server_cn'].encode('binary').ljust(256, "\0"), # 256 bytes
|
19
|
+
self['user_name'].encode('binary').ljust(256, "\0"), # 256 bytes
|
20
|
+
self['password'].encode('binary').ljust(128, "\0"), # 128 bytes
|
21
|
+
self['server_fingerprint'].encode('binary').ljust(256, "\0"), # 256 bytes
|
22
|
+
"\x01\x00".encode('binary'), # 2 bytes
|
23
|
+
].join
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/lib/snxvpn/rsa.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Snxvpn
|
2
|
+
class RSA < OpenSSL::PKey::RSA
|
3
|
+
|
4
|
+
def self.parse(body)
|
5
|
+
mod = body.match(/var *modulus *\= *\'(\w+)\'/).to_a[1]
|
6
|
+
exp = body.match(/var *exponent *\= *\'(\w+)\'/).to_a[1]
|
7
|
+
new(mod, exp) if mod && exp
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(mod, exp)
|
11
|
+
pubkey = create_encoded_pubkey_from([mod].pack("H*"), [exp].pack("H*"))
|
12
|
+
hexkey = [pubkey].pack("H*")
|
13
|
+
pemkey = "-----BEGIN RSA PUBLIC KEY-----\n#{Base64.encode64 hexkey}-----END RSA PUBLIC KEY-----"
|
14
|
+
super(pemkey)
|
15
|
+
end
|
16
|
+
|
17
|
+
def hex_encrypt(s)
|
18
|
+
public_encrypt(s).reverse.unpack("H*").first
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_encoded_pubkey_from(rawmod, rawexp)
|
24
|
+
modulus_hex = prepad_signed(rawmod.unpack('H*').first)
|
25
|
+
exponent_hex = prepad_signed(rawexp.unpack('H*').first)
|
26
|
+
|
27
|
+
modlen = modulus_hex.length / 2
|
28
|
+
explen = exponent_hex.length / 2
|
29
|
+
encoded_modlen = encode_length_hex(modlen)
|
30
|
+
encoded_explen = encode_length_hex(explen)
|
31
|
+
|
32
|
+
full_hex_len = modlen + explen + encoded_modlen.length/2 + encoded_explen.length/2 + 2
|
33
|
+
encoded_hex_len = encode_length_hex(full_hex_len)
|
34
|
+
|
35
|
+
"30#{encoded_hex_len}02#{encoded_modlen}#{modulus_hex}02#{encoded_explen}#{exponent_hex}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def prepad_signed(hex)
|
39
|
+
msb = hex[0]
|
40
|
+
(msb < '0' || msb > '7') ? "00#{hex}" : hex
|
41
|
+
end
|
42
|
+
|
43
|
+
def encode_length_hex(der_len) # encode ASN.1 DER length field
|
44
|
+
return int_to_hex(der_len) if der_len <= 127
|
45
|
+
|
46
|
+
hex = int_to_hex(der_len)
|
47
|
+
size = 128 + hex.length/2
|
48
|
+
"#{int_to_hex(size)}#{hex}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def int_to_hex(num)
|
52
|
+
hex = num.to_s(16)
|
53
|
+
hex.length.odd? ? "0#{hex}" : hex
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: snxvpn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitrij Denissenko
|
@@ -32,6 +32,11 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- bin/snxvpn
|
35
|
+
- lib/snxvpn.rb
|
36
|
+
- lib/snxvpn/cli.rb
|
37
|
+
- lib/snxvpn/config.rb
|
38
|
+
- lib/snxvpn/ext_info.rb
|
39
|
+
- lib/snxvpn/rsa.rb
|
35
40
|
homepage: https://bitbucket.org/bsm/snxvpn
|
36
41
|
licenses: []
|
37
42
|
metadata: {}
|