net-ssh 2.9.2 → 4.0.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
- checksums.yaml.gz.sig +0 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +1129 -0
- data/.travis.yml +41 -5
- data/CHANGES.txt +133 -1
- data/Gemfile +13 -0
- data/Gemfile.norbnacl +10 -0
- data/Gemfile.norbnacl.lock +41 -0
- data/ISSUE_TEMPLATE.md +30 -0
- data/README.rdoc +26 -81
- data/Rakefile +63 -45
- data/appveyor.yml +51 -0
- data/lib/net/ssh/authentication/agent.rb +174 -14
- data/lib/net/ssh/authentication/ed25519.rb +137 -0
- data/lib/net/ssh/authentication/ed25519_loader.rb +21 -0
- data/lib/net/ssh/authentication/key_manager.rb +36 -30
- data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +16 -9
- data/lib/net/ssh/authentication/methods/password.rb +17 -4
- data/lib/net/ssh/authentication/pageant.rb +166 -45
- data/lib/net/ssh/authentication/session.rb +3 -2
- data/lib/net/ssh/buffer.rb +49 -10
- data/lib/net/ssh/buffered_io.rb +17 -12
- data/lib/net/ssh/config.rb +39 -8
- data/lib/net/ssh/connection/channel.rb +42 -20
- data/lib/net/ssh/connection/event_loop.rb +114 -0
- data/lib/net/ssh/connection/keepalive.rb +2 -2
- data/lib/net/ssh/connection/session.rb +120 -34
- data/lib/net/ssh/errors.rb +6 -6
- data/lib/net/ssh/key_factory.rb +49 -43
- data/lib/net/ssh/known_hosts.rb +49 -3
- data/lib/net/ssh/prompt.rb +47 -78
- data/lib/net/ssh/proxy/command.rb +31 -5
- data/lib/net/ssh/proxy/http.rb +15 -11
- data/lib/net/ssh/proxy/https.rb +49 -0
- data/lib/net/ssh/proxy/socks4.rb +2 -1
- data/lib/net/ssh/proxy/socks5.rb +3 -2
- data/lib/net/ssh/ruby_compat.rb +2 -29
- data/lib/net/ssh/service/forward.rb +2 -2
- data/lib/net/ssh/test/channel.rb +7 -0
- data/lib/net/ssh/test/extensions.rb +17 -0
- data/lib/net/ssh/test/kex.rb +4 -4
- data/lib/net/ssh/test/packet.rb +18 -2
- data/lib/net/ssh/test/script.rb +16 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +5 -5
- data/lib/net/ssh/transport/algorithms.rb +92 -75
- data/lib/net/ssh/transport/cipher_factory.rb +19 -26
- data/lib/net/ssh/transport/ctr.rb +7 -9
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +20 -9
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +5 -3
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +1 -1
- data/lib/net/ssh/transport/key_expander.rb +1 -0
- data/lib/net/ssh/transport/openssl.rb +1 -1
- data/lib/net/ssh/transport/packet_stream.rb +11 -3
- data/lib/net/ssh/transport/server_version.rb +13 -6
- data/lib/net/ssh/transport/session.rb +20 -10
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/secure.rb +8 -10
- data/lib/net/ssh/version.rb +4 -4
- data/lib/net/ssh.rb +62 -14
- data/net-ssh-public_cert.pem +19 -18
- data/net-ssh.gemspec +34 -194
- data/support/arcfour_check.rb +1 -1
- data/support/ssh_tunnel_bug.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +125 -109
- metadata.gz.sig +0 -0
- data/Rudyfile +0 -96
- data/lib/net/ssh/authentication/agent/java_pageant.rb +0 -85
- data/lib/net/ssh/authentication/agent/socket.rb +0 -178
- data/setup.rb +0 -1585
- data/test/README.txt +0 -47
- data/test/authentication/methods/common.rb +0 -28
- data/test/authentication/methods/test_abstract.rb +0 -51
- data/test/authentication/methods/test_hostbased.rb +0 -114
- data/test/authentication/methods/test_keyboard_interactive.rb +0 -100
- data/test/authentication/methods/test_none.rb +0 -41
- data/test/authentication/methods/test_password.rb +0 -95
- data/test/authentication/methods/test_publickey.rb +0 -148
- data/test/authentication/test_agent.rb +0 -224
- data/test/authentication/test_key_manager.rb +0 -227
- data/test/authentication/test_session.rb +0 -107
- data/test/common.rb +0 -108
- data/test/configs/auth_off +0 -5
- data/test/configs/auth_on +0 -4
- data/test/configs/empty +0 -0
- data/test/configs/eqsign +0 -3
- data/test/configs/exact_match +0 -8
- data/test/configs/host_plus +0 -10
- data/test/configs/multihost +0 -4
- data/test/configs/negative_match +0 -6
- data/test/configs/nohost +0 -19
- data/test/configs/numeric_host +0 -4
- data/test/configs/send_env +0 -2
- data/test/configs/substitutes +0 -8
- data/test/configs/wild_cards +0 -14
- data/test/connection/test_channel.rb +0 -467
- data/test/connection/test_session.rb +0 -543
- data/test/known_hosts/github +0 -1
- data/test/manual/test_forward.rb +0 -285
- data/test/manual/test_pageant.rb +0 -37
- data/test/start/test_connection.rb +0 -53
- data/test/start/test_options.rb +0 -43
- data/test/start/test_transport.rb +0 -28
- data/test/test_all.rb +0 -11
- data/test/test_buffer.rb +0 -433
- data/test/test_buffered_io.rb +0 -63
- data/test/test_config.rb +0 -221
- data/test/test_key_factory.rb +0 -191
- data/test/test_known_hosts.rb +0 -13
- data/test/transport/hmac/test_md5.rb +0 -41
- data/test/transport/hmac/test_md5_96.rb +0 -27
- data/test/transport/hmac/test_none.rb +0 -34
- data/test/transport/hmac/test_ripemd160.rb +0 -36
- data/test/transport/hmac/test_sha1.rb +0 -36
- data/test/transport/hmac/test_sha1_96.rb +0 -27
- data/test/transport/hmac/test_sha2_256.rb +0 -37
- data/test/transport/hmac/test_sha2_256_96.rb +0 -27
- data/test/transport/hmac/test_sha2_512.rb +0 -37
- data/test/transport/hmac/test_sha2_512_96.rb +0 -27
- data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +0 -13
- data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +0 -146
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +0 -92
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +0 -34
- data/test/transport/kex/test_ecdh_sha2_nistp256.rb +0 -161
- data/test/transport/kex/test_ecdh_sha2_nistp384.rb +0 -38
- data/test/transport/kex/test_ecdh_sha2_nistp521.rb +0 -38
- data/test/transport/test_algorithms.rb +0 -324
- data/test/transport/test_cipher_factory.rb +0 -443
- data/test/transport/test_hmac.rb +0 -34
- data/test/transport/test_identity_cipher.rb +0 -40
- data/test/transport/test_packet_stream.rb +0 -1761
- data/test/transport/test_server_version.rb +0 -78
- data/test/transport/test_session.rb +0 -331
- data/test/transport/test_state.rb +0 -181
|
@@ -2,22 +2,182 @@ require 'net/ssh/buffer'
|
|
|
2
2
|
require 'net/ssh/errors'
|
|
3
3
|
require 'net/ssh/loggable'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
: RUBY_PLATFORM =~ /java/ ? :java : :unix
|
|
5
|
+
require 'net/ssh/transport/server_version'
|
|
6
|
+
require 'socket'
|
|
7
|
+
require 'rubygems'
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
class AgentError < Net::SSH::Exception; end
|
|
9
|
+
require 'net/ssh/authentication/pageant' if Gem.win_platform? && RUBY_PLATFORM != "java"
|
|
12
10
|
|
|
11
|
+
module Net; module SSH; module Authentication
|
|
12
|
+
# Class for representing agent-specific errors.
|
|
13
|
+
class AgentError < Net::SSH::Exception; end
|
|
13
14
|
# An exception for indicating that the SSH agent is not available.
|
|
14
15
|
class AgentNotAvailable < AgentError; end
|
|
15
|
-
end; end; end
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
# This class implements a simple client for the ssh-agent protocol. It
|
|
18
|
+
# does not implement any specific protocol, but instead copies the
|
|
19
|
+
# behavior of the ssh-agent functions in the OpenSSH library (3.8).
|
|
20
|
+
#
|
|
21
|
+
# This means that although it behaves like a SSH1 client, it also has
|
|
22
|
+
# some SSH2 functionality (like signing data).
|
|
23
|
+
class Agent
|
|
24
|
+
include Loggable
|
|
25
|
+
|
|
26
|
+
# A simple module for extending keys, to allow comments to be specified
|
|
27
|
+
# for them.
|
|
28
|
+
module Comment
|
|
29
|
+
attr_accessor :comment
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
SSH2_AGENT_REQUEST_VERSION = 1
|
|
33
|
+
SSH2_AGENT_REQUEST_IDENTITIES = 11
|
|
34
|
+
SSH2_AGENT_IDENTITIES_ANSWER = 12
|
|
35
|
+
SSH2_AGENT_SIGN_REQUEST = 13
|
|
36
|
+
SSH2_AGENT_SIGN_RESPONSE = 14
|
|
37
|
+
SSH2_AGENT_FAILURE = 30
|
|
38
|
+
SSH2_AGENT_VERSION_RESPONSE = 103
|
|
39
|
+
|
|
40
|
+
SSH_COM_AGENT2_FAILURE = 102
|
|
41
|
+
|
|
42
|
+
SSH_AGENT_REQUEST_RSA_IDENTITIES = 1
|
|
43
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2
|
|
44
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5
|
|
45
|
+
SSH_AGENT_FAILURE = 5
|
|
46
|
+
|
|
47
|
+
# The underlying socket being used to communicate with the SSH agent.
|
|
48
|
+
attr_reader :socket
|
|
49
|
+
|
|
50
|
+
# Instantiates a new agent object, connects to a running SSH agent,
|
|
51
|
+
# negotiates the agent protocol version, and returns the agent object.
|
|
52
|
+
def self.connect(logger=nil, agent_socket_factory = nil)
|
|
53
|
+
agent = new(logger)
|
|
54
|
+
agent.connect!(agent_socket_factory)
|
|
55
|
+
agent.negotiate!
|
|
56
|
+
agent
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates a new Agent object, using the optional logger instance to
|
|
60
|
+
# report status.
|
|
61
|
+
def initialize(logger=nil)
|
|
62
|
+
self.logger = logger
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Connect to the agent process using the socket factory and socket name
|
|
66
|
+
# given by the attribute writers. If the agent on the other end of the
|
|
67
|
+
# socket reports that it is an SSH2-compatible agent, this will fail
|
|
68
|
+
# (it only supports the ssh-agent distributed by OpenSSH).
|
|
69
|
+
def connect!(agent_socket_factory = nil)
|
|
70
|
+
debug { "connecting to ssh-agent" }
|
|
71
|
+
@socket =
|
|
72
|
+
if agent_socket_factory
|
|
73
|
+
agent_socket_factory.call
|
|
74
|
+
elsif ENV['SSH_AUTH_SOCK'] && defined?(unix_socket_class)
|
|
75
|
+
unix_socket_class.open(ENV['SSH_AUTH_SOCK'])
|
|
76
|
+
elsif Gem.win_platform? && RUBY_ENGINE != "jruby"
|
|
77
|
+
Pageant::Socket.open
|
|
78
|
+
else
|
|
79
|
+
raise AgentNotAvailable, "Agent not configured"
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
error { "could not connect to ssh-agent: #{e.message}" }
|
|
83
|
+
raise AgentNotAvailable, $!.message
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Attempts to negotiate the SSH agent protocol version. Raises an error
|
|
87
|
+
# if the version could not be negotiated successfully.
|
|
88
|
+
def negotiate!
|
|
89
|
+
# determine what type of agent we're communicating with
|
|
90
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION)
|
|
91
|
+
|
|
92
|
+
raise AgentNotAvailable, "SSH2 agents are not yet supported" if type == SSH2_AGENT_VERSION_RESPONSE
|
|
93
|
+
if type == SSH2_AGENT_FAILURE
|
|
94
|
+
debug { "Unexpected response type==#{type}, this will be ignored" }
|
|
95
|
+
elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
|
|
96
|
+
raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Return an array of all identities (public keys) known to the agent.
|
|
101
|
+
# Each key returned is augmented with a +comment+ property which is set
|
|
102
|
+
# to the comment returned by the agent for that key.
|
|
103
|
+
def identities
|
|
104
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES)
|
|
105
|
+
raise AgentError, "could not get identity count" if agent_failed(type)
|
|
106
|
+
raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER
|
|
107
|
+
|
|
108
|
+
identities = []
|
|
109
|
+
body.read_long.times do
|
|
110
|
+
key_str = body.read_string
|
|
111
|
+
comment_str = body.read_string
|
|
112
|
+
begin
|
|
113
|
+
key = Buffer.new(key_str).read_key
|
|
114
|
+
key.extend(Comment)
|
|
115
|
+
key.comment = comment_str
|
|
116
|
+
identities.push key
|
|
117
|
+
rescue NotImplementedError => e
|
|
118
|
+
error { "ignoring unimplemented key:#{e.message} #{comment_str}" }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return identities
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Closes this socket. This agent reference is no longer able to
|
|
126
|
+
# query the agent.
|
|
127
|
+
def close
|
|
128
|
+
@socket.close
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Using the agent and the given public key, sign the given data. The
|
|
132
|
+
# signature is returned in SSH2 format.
|
|
133
|
+
def sign(key, data)
|
|
134
|
+
type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0)
|
|
135
|
+
|
|
136
|
+
raise AgentError, "agent could not sign data with requested identity" if agent_failed(type)
|
|
137
|
+
raise AgentError, "bad authentication response #{type}" if type != SSH2_AGENT_SIGN_RESPONSE
|
|
138
|
+
|
|
139
|
+
return reply.read_string
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def unix_socket_class
|
|
145
|
+
UNIXSocket
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Send a new packet of the given type, with the associated data.
|
|
149
|
+
def send_packet(type, *args)
|
|
150
|
+
buffer = Buffer.from(*args)
|
|
151
|
+
data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*")
|
|
152
|
+
debug { "sending agent request #{type} len #{buffer.length}" }
|
|
153
|
+
@socket.send data, 0
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Read the next packet from the agent. This will return a two-part
|
|
157
|
+
# tuple consisting of the packet type, and the packet's body (which
|
|
158
|
+
# is returned as a Net::SSH::Buffer).
|
|
159
|
+
def read_packet
|
|
160
|
+
buffer = Net::SSH::Buffer.new(@socket.read(4))
|
|
161
|
+
buffer.append(@socket.read(buffer.read_long))
|
|
162
|
+
type = buffer.read_byte
|
|
163
|
+
debug { "received agent packet #{type} len #{buffer.length-4}" }
|
|
164
|
+
return type, buffer
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Send the given packet and return the subsequent reply from the agent.
|
|
168
|
+
# (See #send_packet and #read_packet).
|
|
169
|
+
def send_and_wait(type, *args)
|
|
170
|
+
send_packet(type, *args)
|
|
171
|
+
read_packet
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Returns +true+ if the parameter indicates a "failure" response from
|
|
175
|
+
# the agent, and +false+ otherwise.
|
|
176
|
+
def agent_failed(type)
|
|
177
|
+
type == SSH_AGENT_FAILURE ||
|
|
178
|
+
type == SSH2_AGENT_FAILURE ||
|
|
179
|
+
type == SSH_COM_AGENT2_FAILURE
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
end; end; end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
gem 'rbnacl-libsodium', '>= 1.0.10'
|
|
2
|
+
gem 'rbnacl', '>= 3.2.0', '< 4.0'
|
|
3
|
+
gem 'bcrypt_pbkdf', '~> 1.0.0' unless RUBY_PLATFORM == "java"
|
|
4
|
+
|
|
5
|
+
require 'rbnacl/libsodium'
|
|
6
|
+
require 'rbnacl'
|
|
7
|
+
require 'rbnacl/signatures/ed25519/verify_key'
|
|
8
|
+
require 'rbnacl/signatures/ed25519/signing_key'
|
|
9
|
+
|
|
10
|
+
require 'rbnacl/hash'
|
|
11
|
+
|
|
12
|
+
require 'base64'
|
|
13
|
+
|
|
14
|
+
require 'net/ssh/transport/cipher_factory'
|
|
15
|
+
require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java"
|
|
16
|
+
|
|
17
|
+
module Net; module SSH; module Authentication
|
|
18
|
+
module ED25519
|
|
19
|
+
class SigningKeyFromFile < RbNaCl::Signatures::Ed25519::SigningKey
|
|
20
|
+
def initialize(pk,sk)
|
|
21
|
+
@signing_key = sk
|
|
22
|
+
@verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(pk)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class PubKey
|
|
27
|
+
def initialize(data)
|
|
28
|
+
@verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(data)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.read_keyblob(buffer)
|
|
32
|
+
PubKey.new(buffer.read_string)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_blob
|
|
36
|
+
Net::SSH::Buffer.from(:mstring,"ssh-ed25519",:string,@verify_key.to_bytes).to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ssh_type
|
|
40
|
+
"ssh-ed25519"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ssh_do_verify(sig,data)
|
|
44
|
+
@verify_key.verify(sig,data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_pem
|
|
48
|
+
# TODO this is not pem
|
|
49
|
+
ssh_type + Base64.encode64(@verify_key.to_bytes)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fingerprint
|
|
53
|
+
@fingerprint ||= OpenSSL::Digest::MD5.hexdigest(to_blob).scan(/../).join(":")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class PrivKey
|
|
58
|
+
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
59
|
+
|
|
60
|
+
MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
|
|
61
|
+
MEND = "-----END OPENSSH PRIVATE KEY-----\n"
|
|
62
|
+
MAGIC = "openssh-key-v1"
|
|
63
|
+
|
|
64
|
+
def initialize(datafull,password)
|
|
65
|
+
raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
|
|
66
|
+
raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
|
|
67
|
+
datab64 = datafull[MBEGIN.size ... -MEND.size]
|
|
68
|
+
data = Base64.decode64(datab64)
|
|
69
|
+
raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
|
|
70
|
+
buffer = Net::SSH::Buffer.new(data[MAGIC.size+1 .. -1])
|
|
71
|
+
|
|
72
|
+
ciphername = buffer.read_string
|
|
73
|
+
raise ArgumentError.new("#{ciphername} in private key is not supported") unless
|
|
74
|
+
CipherFactory.supported?(ciphername)
|
|
75
|
+
|
|
76
|
+
kdfname = buffer.read_string
|
|
77
|
+
raise ArgumentError.new("Expected #{kdfname} to be or none or bcrypt") unless %w(none bcrypt).include?(kdfname)
|
|
78
|
+
|
|
79
|
+
kdfopts = Net::SSH::Buffer.new(buffer.read_string)
|
|
80
|
+
num_keys = buffer.read_long
|
|
81
|
+
raise ArgumentError.new("Only 1 key is supported in ssh keys #{num_keys} was in private key") unless num_keys == 1
|
|
82
|
+
_pubkey = buffer.read_string
|
|
83
|
+
|
|
84
|
+
len = buffer.read_long
|
|
85
|
+
|
|
86
|
+
keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
|
|
87
|
+
raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
|
|
88
|
+
((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0))
|
|
89
|
+
|
|
90
|
+
if kdfname == 'bcrypt'
|
|
91
|
+
salt = kdfopts.read_string
|
|
92
|
+
rounds = kdfopts.read_long
|
|
93
|
+
|
|
94
|
+
raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
|
|
95
|
+
key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
|
|
96
|
+
else
|
|
97
|
+
key = '\x00' * (keylen + ivlen)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv:key[keylen...keylen+ivlen], decrypt: true)
|
|
101
|
+
|
|
102
|
+
decoded = cipher.update(buffer.remainder_as_buffer.to_s)
|
|
103
|
+
decoded << cipher.final
|
|
104
|
+
|
|
105
|
+
decoded = Net::SSH::Buffer.new(decoded)
|
|
106
|
+
check1 = decoded.read_long
|
|
107
|
+
check2 = decoded.read_long
|
|
108
|
+
|
|
109
|
+
raise ArgumentError, "Decrypt failed on private key" if (check1 != check2)
|
|
110
|
+
|
|
111
|
+
_type_name = decoded.read_string
|
|
112
|
+
pk = decoded.read_string
|
|
113
|
+
sk = decoded.read_string
|
|
114
|
+
_comment = decoded.read_string
|
|
115
|
+
|
|
116
|
+
@pk = pk
|
|
117
|
+
@sign_key = SigningKeyFromFile.new(pk,sk)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def public_key
|
|
121
|
+
PubKey.new(@pk)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ssh_do_sign(data)
|
|
125
|
+
@sign_key.sign(data)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.read(data,password)
|
|
129
|
+
self.new(data,password)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.read_keyblob(buffer)
|
|
133
|
+
ED25519::PubKey.read_keyblob(buffer)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end; end; end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Net; module SSH; module Authentication
|
|
2
|
+
|
|
3
|
+
# Loads ED25519 support which requires optinal dependecies like
|
|
4
|
+
# rbnacl-libsodium, rbnacl, bcrypt_pbkdf
|
|
5
|
+
module ED25519Loader
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'net/ssh/authentication/ed25519'
|
|
9
|
+
LOADED = true
|
|
10
|
+
ERROR = nil
|
|
11
|
+
rescue LoadError => e
|
|
12
|
+
ERROR = e
|
|
13
|
+
LOADED = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.raiseUnlessLoaded(message)
|
|
17
|
+
raise NotImplementedError, "#{message} -- see #{ERROR}" unless LOADED
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end; end; end
|
|
@@ -98,7 +98,7 @@ module Net
|
|
|
98
98
|
def each_identity
|
|
99
99
|
prepared_identities = prepare_identities_from_files + prepare_identities_from_data
|
|
100
100
|
|
|
101
|
-
user_identities = load_identities(prepared_identities, false)
|
|
101
|
+
user_identities = load_identities(prepared_identities, false, true)
|
|
102
102
|
|
|
103
103
|
if agent
|
|
104
104
|
agent.identities.each do |key|
|
|
@@ -108,13 +108,13 @@ module Net
|
|
|
108
108
|
user_identities.delete(corresponding_user_identity) if corresponding_user_identity
|
|
109
109
|
|
|
110
110
|
if !options[:keys_only] || corresponding_user_identity
|
|
111
|
-
known_identities[key] = { :
|
|
111
|
+
known_identities[key] = { from: :agent }
|
|
112
112
|
yield key
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
user_identities = load_identities(user_identities,
|
|
117
|
+
user_identities = load_identities(user_identities, !options[:non_interactive], false)
|
|
118
118
|
|
|
119
119
|
user_identities.each do |identity|
|
|
120
120
|
key = identity.delete(:public_key)
|
|
@@ -139,15 +139,15 @@ module Net
|
|
|
139
139
|
|
|
140
140
|
if info[:key].nil? && info[:from] == :file
|
|
141
141
|
begin
|
|
142
|
-
info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase],
|
|
143
|
-
rescue
|
|
142
|
+
info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase], !options[:non_interactive])
|
|
143
|
+
rescue OpenSSL::OpenSSLError, Exception => e
|
|
144
144
|
raise KeyManagerError, "the given identity is known, but the private key could not be loaded: #{e.class} (#{e.message})"
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
if info[:key]
|
|
149
149
|
return Net::SSH::Buffer.from(:string, identity.ssh_type,
|
|
150
|
-
:
|
|
150
|
+
:mstring, info[:key].ssh_do_sign(data.to_s)).to_s
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
if info[:from] == :agent
|
|
@@ -176,7 +176,7 @@ module Net
|
|
|
176
176
|
# or if the agent is otherwise not available.
|
|
177
177
|
def agent
|
|
178
178
|
return unless use_agent?
|
|
179
|
-
@agent ||= Agent.connect(logger)
|
|
179
|
+
@agent ||= Agent.connect(logger, options[:agent_socket_factory])
|
|
180
180
|
rescue AgentNotAvailable
|
|
181
181
|
@use_agent = false
|
|
182
182
|
nil
|
|
@@ -187,52 +187,58 @@ module Net
|
|
|
187
187
|
# Prepares identities from user key_files for loading, preserving their order and sources.
|
|
188
188
|
def prepare_identities_from_files
|
|
189
189
|
key_files.map do |file|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
if readable_file?(file)
|
|
191
|
+
identity = {}
|
|
192
|
+
public_key_file = file + ".pub"
|
|
193
|
+
if readable_file?(public_key_file)
|
|
194
|
+
identity[:load_from] = :pubkey_file
|
|
195
|
+
identity[:pubkey_file] = public_key_file
|
|
196
|
+
else
|
|
197
|
+
identity[:load_from] = :privkey_file
|
|
198
|
+
end
|
|
199
|
+
identity.merge(privkey_file: file)
|
|
195
200
|
end
|
|
196
201
|
end.compact
|
|
197
202
|
end
|
|
198
203
|
|
|
204
|
+
def readable_file?(path)
|
|
205
|
+
File.file?(path) && File.readable?(path)
|
|
206
|
+
end
|
|
207
|
+
|
|
199
208
|
# Prepared identities from user key_data, preserving their order and sources.
|
|
200
209
|
def prepare_identities_from_data
|
|
201
210
|
key_data.map do |data|
|
|
202
|
-
{ :
|
|
211
|
+
{ load_from: :data, data: data }
|
|
203
212
|
end
|
|
204
213
|
end
|
|
205
214
|
|
|
206
|
-
# Load prepared identities. Private key decryption errors ignored if
|
|
207
|
-
def load_identities(identities, ask_passphrase)
|
|
215
|
+
# Load prepared identities. Private key decryption errors ignored if ignore_decryption_errors
|
|
216
|
+
def load_identities(identities, ask_passphrase, ignore_decryption_errors)
|
|
208
217
|
identities.map do |identity|
|
|
209
218
|
begin
|
|
210
219
|
case identity[:load_from]
|
|
211
220
|
when :pubkey_file
|
|
212
|
-
key = KeyFactory.load_public_key(identity[:
|
|
213
|
-
{ :
|
|
221
|
+
key = KeyFactory.load_public_key(identity[:pubkey_file])
|
|
222
|
+
{ public_key: key, from: :file, file: identity[:privkey_file] }
|
|
214
223
|
when :privkey_file
|
|
215
|
-
private_key = KeyFactory.load_private_key(identity[:
|
|
224
|
+
private_key = KeyFactory.load_private_key(identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt])
|
|
216
225
|
key = private_key.send(:public_key)
|
|
217
|
-
{ :
|
|
226
|
+
{ public_key: key, from: :file, file: identity[:privkey_file], key: private_key }
|
|
218
227
|
when :data
|
|
219
|
-
private_key = KeyFactory.load_data_private_key(identity[:data], options[:passphrase], ask_passphrase)
|
|
228
|
+
private_key = KeyFactory.load_data_private_key(identity[:data], options[:passphrase], ask_passphrase, "<key in memory>", options[:password_prompt])
|
|
220
229
|
key = private_key.send(:public_key)
|
|
221
|
-
{ :
|
|
230
|
+
{ public_key: key, from: :key_data, data: identity[:data], key: private_key }
|
|
222
231
|
else
|
|
223
232
|
identity
|
|
224
233
|
end
|
|
225
234
|
|
|
226
|
-
rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError, OpenSSL::PKey::ECError => e
|
|
227
|
-
if
|
|
235
|
+
rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError, OpenSSL::PKey::ECError, OpenSSL::PKey::PKeyError, ArgumentError => e
|
|
236
|
+
if ignore_decryption_errors
|
|
237
|
+
identity
|
|
238
|
+
else
|
|
228
239
|
process_identity_loading_error(identity, e)
|
|
229
240
|
nil
|
|
230
|
-
else
|
|
231
|
-
identity
|
|
232
241
|
end
|
|
233
|
-
rescue ArgumentError => e
|
|
234
|
-
process_identity_loading_error(identity, e)
|
|
235
|
-
nil
|
|
236
242
|
rescue Exception => e
|
|
237
243
|
process_identity_loading_error(identity, e)
|
|
238
244
|
nil
|
|
@@ -243,9 +249,9 @@ module Net
|
|
|
243
249
|
def process_identity_loading_error(identity, e)
|
|
244
250
|
case identity[:load_from]
|
|
245
251
|
when :pubkey_file
|
|
246
|
-
error { "could not load public key file `#{identity[:
|
|
252
|
+
error { "could not load public key file `#{identity[:pubkey_file]}': #{e.class} (#{e.message})" }
|
|
247
253
|
when :privkey_file
|
|
248
|
-
error { "could not load private key file `#{identity[:
|
|
254
|
+
error { "could not load private key file `#{identity[:privkey_file]}': #{e.class} (#{e.message})" }
|
|
249
255
|
else
|
|
250
256
|
raise e
|
|
251
257
|
end
|
|
@@ -22,6 +22,7 @@ module Net; module SSH; module Authentication; module Methods
|
|
|
22
22
|
@session = session
|
|
23
23
|
@key_manager = options[:key_manager]
|
|
24
24
|
@options = options
|
|
25
|
+
@prompt = options[:password_prompt]
|
|
25
26
|
self.logger = session.logger
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -55,6 +56,9 @@ module Net; module SSH; module Authentication; module Methods
|
|
|
55
56
|
buffer
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
attr_reader :prompt
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
end; end; end; end
|
|
@@ -8,8 +8,6 @@ module Net
|
|
|
8
8
|
|
|
9
9
|
# Implements the "keyboard-interactive" SSH authentication method.
|
|
10
10
|
class KeyboardInteractive < Abstract
|
|
11
|
-
include Prompt
|
|
12
|
-
|
|
13
11
|
USERAUTH_INFO_REQUEST = 60
|
|
14
12
|
USERAUTH_INFO_RESPONSE = 61
|
|
15
13
|
|
|
@@ -18,12 +16,14 @@ module Net
|
|
|
18
16
|
debug { "trying keyboard-interactive" }
|
|
19
17
|
send_message(userauth_request(username, next_service, "keyboard-interactive", "", ""))
|
|
20
18
|
|
|
19
|
+
prompter = nil
|
|
21
20
|
loop do
|
|
22
21
|
message = session.next_message
|
|
23
22
|
|
|
24
23
|
case message.type
|
|
25
24
|
when USERAUTH_SUCCESS
|
|
26
25
|
debug { "keyboard-interactive succeeded" }
|
|
26
|
+
prompter.success if prompter
|
|
27
27
|
return true
|
|
28
28
|
when USERAUTH_FAILURE
|
|
29
29
|
debug { "keyboard-interactive failed" }
|
|
@@ -31,24 +31,27 @@ module Net
|
|
|
31
31
|
raise Net::SSH::Authentication::DisallowedMethod unless
|
|
32
32
|
message[:authentications].split(/,/).include? 'keyboard-interactive'
|
|
33
33
|
|
|
34
|
-
return false
|
|
34
|
+
return false unless interactive?
|
|
35
|
+
password = nil
|
|
36
|
+
debug { "retrying keyboard-interactive" }
|
|
37
|
+
send_message(userauth_request(username, next_service, "keyboard-interactive", "", ""))
|
|
35
38
|
when USERAUTH_INFO_REQUEST
|
|
36
39
|
name = message.read_string
|
|
37
40
|
instruction = message.read_string
|
|
38
41
|
debug { "keyboard-interactive info request" }
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
puts(instruction) unless instruction.empty?
|
|
43
|
+
if password.nil? && interactive? && prompter.nil?
|
|
44
|
+
prompter = prompt.start(type: 'keyboard-interactive', name: name, instruction: instruction)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
_ = message.read_string # lang_tag
|
|
46
48
|
responses =[]
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
message.read_long.times do
|
|
49
51
|
text = message.read_string
|
|
50
52
|
echo = message.read_bool
|
|
51
|
-
|
|
53
|
+
password_to_send = password || (prompter && prompter.ask(text, echo))
|
|
54
|
+
responses << password_to_send
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
# if the password failed the first time around, don't try
|
|
@@ -62,8 +65,12 @@ module Net
|
|
|
62
65
|
end
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
|
-
end
|
|
66
68
|
|
|
69
|
+
def interactive?
|
|
70
|
+
options = session.transport.options || {}
|
|
71
|
+
!options[:non_interactive]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
67
74
|
end
|
|
68
75
|
end
|
|
69
76
|
end
|
|
@@ -9,11 +9,10 @@ module Net
|
|
|
9
9
|
|
|
10
10
|
# Implements the "password" SSH authentication method.
|
|
11
11
|
class Password < Abstract
|
|
12
|
-
include Prompt
|
|
13
|
-
|
|
14
12
|
# Attempt to authenticate the given user for the given service. If
|
|
15
13
|
# the password parameter is nil, this will ask for password
|
|
16
14
|
def authenticate(next_service, username, password=nil)
|
|
15
|
+
clear_prompter!
|
|
17
16
|
retries = 0
|
|
18
17
|
max_retries = get_max_retries
|
|
19
18
|
return false if !password && max_retries == 0
|
|
@@ -37,6 +36,7 @@ module Net
|
|
|
37
36
|
case message.type
|
|
38
37
|
when USERAUTH_SUCCESS
|
|
39
38
|
debug { "password succeeded" }
|
|
39
|
+
@prompter.success if @prompter
|
|
40
40
|
return true
|
|
41
41
|
when USERAUTH_FAILURE
|
|
42
42
|
return false
|
|
@@ -52,13 +52,26 @@ module Net
|
|
|
52
52
|
|
|
53
53
|
NUMBER_OF_PASSWORD_PROMPTS = 3
|
|
54
54
|
|
|
55
|
+
def clear_prompter!
|
|
56
|
+
@prompt_info = nil
|
|
57
|
+
@prompter = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
def ask_password(username)
|
|
61
|
+
host = session.transport.host
|
|
62
|
+
prompt_info = {type: 'password', user: username, host: host}
|
|
63
|
+
if @prompt_info != prompt_info
|
|
64
|
+
@prompt_info = prompt_info
|
|
65
|
+
@prompter = prompt.start(prompt_info)
|
|
66
|
+
end
|
|
56
67
|
echo = false
|
|
57
|
-
|
|
68
|
+
@prompter.ask("#{username}@#{host}'s password:", echo)
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
def get_max_retries
|
|
61
|
-
|
|
72
|
+
options = session.transport.options || {}
|
|
73
|
+
result = options[:number_of_password_prompts] || NUMBER_OF_PASSWORD_PROMPTS
|
|
74
|
+
options[:non_interactive] ? 0 : result
|
|
62
75
|
end
|
|
63
76
|
end
|
|
64
77
|
|