net-ssh 2.9.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +1129 -0
  6. data/.travis.yml +41 -5
  7. data/CHANGES.txt +133 -1
  8. data/Gemfile +13 -0
  9. data/Gemfile.norbnacl +10 -0
  10. data/Gemfile.norbnacl.lock +41 -0
  11. data/ISSUE_TEMPLATE.md +30 -0
  12. data/README.rdoc +26 -81
  13. data/Rakefile +63 -45
  14. data/appveyor.yml +51 -0
  15. data/lib/net/ssh/authentication/agent.rb +174 -14
  16. data/lib/net/ssh/authentication/ed25519.rb +137 -0
  17. data/lib/net/ssh/authentication/ed25519_loader.rb +21 -0
  18. data/lib/net/ssh/authentication/key_manager.rb +36 -30
  19. data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
  20. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +16 -9
  21. data/lib/net/ssh/authentication/methods/password.rb +17 -4
  22. data/lib/net/ssh/authentication/pageant.rb +166 -45
  23. data/lib/net/ssh/authentication/session.rb +3 -2
  24. data/lib/net/ssh/buffer.rb +49 -10
  25. data/lib/net/ssh/buffered_io.rb +17 -12
  26. data/lib/net/ssh/config.rb +39 -8
  27. data/lib/net/ssh/connection/channel.rb +42 -20
  28. data/lib/net/ssh/connection/event_loop.rb +114 -0
  29. data/lib/net/ssh/connection/keepalive.rb +2 -2
  30. data/lib/net/ssh/connection/session.rb +120 -34
  31. data/lib/net/ssh/errors.rb +6 -6
  32. data/lib/net/ssh/key_factory.rb +49 -43
  33. data/lib/net/ssh/known_hosts.rb +49 -3
  34. data/lib/net/ssh/prompt.rb +47 -78
  35. data/lib/net/ssh/proxy/command.rb +31 -5
  36. data/lib/net/ssh/proxy/http.rb +15 -11
  37. data/lib/net/ssh/proxy/https.rb +49 -0
  38. data/lib/net/ssh/proxy/socks4.rb +2 -1
  39. data/lib/net/ssh/proxy/socks5.rb +3 -2
  40. data/lib/net/ssh/ruby_compat.rb +2 -29
  41. data/lib/net/ssh/service/forward.rb +2 -2
  42. data/lib/net/ssh/test/channel.rb +7 -0
  43. data/lib/net/ssh/test/extensions.rb +17 -0
  44. data/lib/net/ssh/test/kex.rb +4 -4
  45. data/lib/net/ssh/test/packet.rb +18 -2
  46. data/lib/net/ssh/test/script.rb +16 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +5 -5
  49. data/lib/net/ssh/transport/algorithms.rb +92 -75
  50. data/lib/net/ssh/transport/cipher_factory.rb +19 -26
  51. data/lib/net/ssh/transport/ctr.rb +7 -9
  52. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +20 -9
  53. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +5 -3
  54. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +1 -1
  55. data/lib/net/ssh/transport/key_expander.rb +1 -0
  56. data/lib/net/ssh/transport/openssl.rb +1 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +11 -3
  58. data/lib/net/ssh/transport/server_version.rb +13 -6
  59. data/lib/net/ssh/transport/session.rb +20 -10
  60. data/lib/net/ssh/transport/state.rb +1 -1
  61. data/lib/net/ssh/verifiers/secure.rb +8 -10
  62. data/lib/net/ssh/version.rb +4 -4
  63. data/lib/net/ssh.rb +62 -14
  64. data/net-ssh-public_cert.pem +19 -18
  65. data/net-ssh.gemspec +34 -194
  66. data/support/arcfour_check.rb +1 -1
  67. data/support/ssh_tunnel_bug.rb +1 -1
  68. data.tar.gz.sig +0 -0
  69. metadata +125 -109
  70. metadata.gz.sig +0 -0
  71. data/Rudyfile +0 -96
  72. data/lib/net/ssh/authentication/agent/java_pageant.rb +0 -85
  73. data/lib/net/ssh/authentication/agent/socket.rb +0 -178
  74. data/setup.rb +0 -1585
  75. data/test/README.txt +0 -47
  76. data/test/authentication/methods/common.rb +0 -28
  77. data/test/authentication/methods/test_abstract.rb +0 -51
  78. data/test/authentication/methods/test_hostbased.rb +0 -114
  79. data/test/authentication/methods/test_keyboard_interactive.rb +0 -100
  80. data/test/authentication/methods/test_none.rb +0 -41
  81. data/test/authentication/methods/test_password.rb +0 -95
  82. data/test/authentication/methods/test_publickey.rb +0 -148
  83. data/test/authentication/test_agent.rb +0 -224
  84. data/test/authentication/test_key_manager.rb +0 -227
  85. data/test/authentication/test_session.rb +0 -107
  86. data/test/common.rb +0 -108
  87. data/test/configs/auth_off +0 -5
  88. data/test/configs/auth_on +0 -4
  89. data/test/configs/empty +0 -0
  90. data/test/configs/eqsign +0 -3
  91. data/test/configs/exact_match +0 -8
  92. data/test/configs/host_plus +0 -10
  93. data/test/configs/multihost +0 -4
  94. data/test/configs/negative_match +0 -6
  95. data/test/configs/nohost +0 -19
  96. data/test/configs/numeric_host +0 -4
  97. data/test/configs/send_env +0 -2
  98. data/test/configs/substitutes +0 -8
  99. data/test/configs/wild_cards +0 -14
  100. data/test/connection/test_channel.rb +0 -467
  101. data/test/connection/test_session.rb +0 -543
  102. data/test/known_hosts/github +0 -1
  103. data/test/manual/test_forward.rb +0 -285
  104. data/test/manual/test_pageant.rb +0 -37
  105. data/test/start/test_connection.rb +0 -53
  106. data/test/start/test_options.rb +0 -43
  107. data/test/start/test_transport.rb +0 -28
  108. data/test/test_all.rb +0 -11
  109. data/test/test_buffer.rb +0 -433
  110. data/test/test_buffered_io.rb +0 -63
  111. data/test/test_config.rb +0 -221
  112. data/test/test_key_factory.rb +0 -191
  113. data/test/test_known_hosts.rb +0 -13
  114. data/test/transport/hmac/test_md5.rb +0 -41
  115. data/test/transport/hmac/test_md5_96.rb +0 -27
  116. data/test/transport/hmac/test_none.rb +0 -34
  117. data/test/transport/hmac/test_ripemd160.rb +0 -36
  118. data/test/transport/hmac/test_sha1.rb +0 -36
  119. data/test/transport/hmac/test_sha1_96.rb +0 -27
  120. data/test/transport/hmac/test_sha2_256.rb +0 -37
  121. data/test/transport/hmac/test_sha2_256_96.rb +0 -27
  122. data/test/transport/hmac/test_sha2_512.rb +0 -37
  123. data/test/transport/hmac/test_sha2_512_96.rb +0 -27
  124. data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +0 -13
  125. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +0 -146
  126. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +0 -92
  127. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +0 -34
  128. data/test/transport/kex/test_ecdh_sha2_nistp256.rb +0 -161
  129. data/test/transport/kex/test_ecdh_sha2_nistp384.rb +0 -38
  130. data/test/transport/kex/test_ecdh_sha2_nistp521.rb +0 -38
  131. data/test/transport/test_algorithms.rb +0 -324
  132. data/test/transport/test_cipher_factory.rb +0 -443
  133. data/test/transport/test_hmac.rb +0 -34
  134. data/test/transport/test_identity_cipher.rb +0 -40
  135. data/test/transport/test_packet_stream.rb +0 -1761
  136. data/test/transport/test_server_version.rb +0 -78
  137. data/test/transport/test_session.rb +0 -331
  138. 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
- module Net; module SSH; module Authentication
6
- PLATFORM = File::ALT_SEPARATOR \
7
- ? RUBY_PLATFORM =~ /java/ ? :java_win32 : :win32 \
8
- : RUBY_PLATFORM =~ /java/ ? :java : :unix
5
+ require 'net/ssh/transport/server_version'
6
+ require 'socket'
7
+ require 'rubygems'
9
8
 
10
- # A trivial exception class for representing agent-specific errors.
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
- case Net::SSH::Authentication::PLATFORM
18
- when :java_win32
19
- # Java pageant requires whole different agent.
20
- require 'net/ssh/authentication/agent/java_pageant'
21
- else
22
- require 'net/ssh/authentication/agent/socket'
23
- end
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] = { :from => :agent }
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, true)
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], true)
143
- rescue Exception, OpenSSL::OpenSSLError => e
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
- :string, info[:key].ssh_do_sign(data.to_s)).to_s
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
- public_key_file = file + ".pub"
191
- if File.readable?(public_key_file)
192
- { :load_from => :pubkey_file, :file => file }
193
- elsif File.readable?(file)
194
- { :load_from => :privkey_file, :file => file }
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
- { :load_from => :data, :data => data }
211
+ { load_from: :data, data: data }
203
212
  end
204
213
  end
205
214
 
206
- # Load prepared identities. Private key decryption errors ignored if passphrase was not prompted.
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[:file] + ".pub")
213
- { :public_key => key, :from => :file, :file => identity[:file] }
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[:file], options[:passphrase], ask_passphrase)
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
- { :public_key => key, :from => :file, :file => identity[:file], :key => private_key }
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
- { :public_key => key, :from => :key_data, :data => identity[:data], :key => private_key }
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 ask_passphrase
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[:file]}': #{e.class} (#{e.message})" }
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[:file]}': #{e.class} (#{e.message})" }
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
- unless password
41
- puts(name) unless name.empty?
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
- responses << (password || prompt(text, echo))
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
- prompt("#{username}@#{session.transport.host}'s password:", echo)
68
+ @prompter.ask("#{username}@#{host}'s password:", echo)
58
69
  end
59
70
 
60
71
  def get_max_retries
61
- (session.transport.options||{})[:number_of_password_prompts] || NUMBER_OF_PASSWORD_PROMPTS
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