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.
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