1
+ # Note: Logger.warn exceptions because the :ssh_client_key_api will silently
2
+ # catches exceptions
1
3
defmodule SSHClientKeyAPI do
2
4
@ external_resource "README.md"
3
5
@ moduledoc "README.md"
4
6
|> File . read! ( )
5
7
|> String . split ( "<!-- MDOC !-->" )
6
8
|> Enum . fetch! ( 1 )
7
9
8
- alias SSHClientKeyAPI.KeyError
10
+ require Logger
11
+ require Record
9
12
10
13
@ behaviour :ssh_client_key_api
11
- @ key_algorithms :ssh . default_algorithms ( ) [ :public_key ]
12
14
13
15
@ doc """
14
- Returns a tuple suitable for passing the `SSHKit.SSH.connect/2` as the `key_cb` option.
16
+ Returns a tuple suitable for passing to `:ssh.connect/3` or
17
+ `SSHKit.SSH.connect/2` as the `key_cb` option.
15
18
16
19
## Options
17
20
@@ -35,7 +38,6 @@ defmodule SSHClientKeyAPI do
35
38
SSHKit.SSH.connect("example.com", key_cb: cb)
36
39
37
40
"""
38
- @ spec with_options ( opts :: list ) :: { atom , list }
39
41
def with_options ( opts \\ [ ] ) do
40
42
opts = with_defaults ( opts )
41
43
@@ -47,14 +49,17 @@ defmodule SSHClientKeyAPI do
47
49
{ __MODULE__ , opts }
48
50
end
49
51
50
- def add_host_key ( hostname , key , opts ) do
52
+ @ impl :ssh_client_key_api
53
+ def add_host_key ( hostname , _port , key , opts ) do
54
+ hostname = normalize_hostname ( hostname )
55
+
51
56
case silently_accept_hosts ( opts ) do
52
57
true ->
53
58
opts
54
- |> known_hosts_data
55
- |> :public_key . ssh_decode ( :known_hosts )
59
+ |> known_hosts_data ( )
60
+ |> :ssh_file . decode ( :known_hosts )
56
61
|> ( fn decoded -> decoded ++ [ { key , [ { :hostnames , [ hostname ] } ] } ] end ) . ( )
57
- |> :public_key . ssh_encode ( :known_hosts )
62
+ |> :ssh_file . encode ( :known_hosts )
58
63
|> ( fn encoded -> IO . binwrite ( known_hosts ( opts ) , encoded ) end ) . ( )
59
64
60
65
_ ->
@@ -66,83 +71,115 @@ defmodule SSHClientKeyAPI do
66
71
67
72
{ :error , message }
68
73
end
74
+ rescue
75
+ e ->
76
+ Logger . warn ( "Exception in add_host_key: #{ inspect ( e ) } " )
77
+ raise e
69
78
end
70
79
71
- def is_host_key ( key , hostname , alg , opts ) when alg in @ key_algorithms do
72
- silently_accept_hosts ( opts ) ||
80
+ @ impl :ssh_client_key_api
81
+ def is_host_key ( key , host , port , alg , opts ) do
82
+ :ssh_file . is_host_key ( key , host , port , alg , opts )
83
+ rescue
84
+ e ->
85
+ Logger . warn ( "Exception in is_host_key: #{ inspect ( e ) } " )
86
+ end
87
+
88
+ # There's a fundamental disconnect between how the key_cb option works and how
89
+ # we want to use it. The key_cb option is expecting us to receive the
90
+ # algorithm type and then find the matching key, but we already know the exact
91
+ # key we want to use. So instead we return the key for every algorithm type
92
+ # and erlang will ignore the keys we return for an incorrect algorithm type.
93
+ #
94
+ # Ideally we could instead find the matching algorithm type for the key
95
+ # provided by the user without requiring the user to manually provide the key
96
+ # type but thus far I've been unable to find a way to find the algorithm type
97
+ # for the key
98
+ @ impl :ssh_client_key_api
99
+ def user_key ( _alg , opts ) do
100
+ raw_key =
73
101
opts
74
- |> known_hosts_data
75
- |> to_string
76
- |> :public_key . ssh_decode ( :known_hosts )
77
- |> has_fingerprint ( key , hostname )
78
- end
102
+ |> identity_data ( )
103
+ |> to_string ( )
79
104
80
- def is_host_key ( _ , _ , alg , _ ) do
81
- IO . puts ( "unsupported host key algorithm #{ inspect ( alg ) } " )
82
- false
83
- end
84
-
85
- def user_key ( alg , opts ) when alg in @ key_algorithms do
86
- opts
87
- |> identity_data
88
- |> to_string
105
+ raw_key
89
106
|> :public_key . pem_decode ( )
90
107
|> List . first ( )
91
- |> decode_pem_entry ( passphrase ( opts ) )
92
- end
108
+ |> case do
109
+ { { :no_asn1 , :new_openssh } , _data , :not_encrypted } ->
110
+ :ssh_file . decode ( raw_key , :public_key )
111
+ |> case do
112
+ [ { key , _comments } | _rest ] ->
113
+ { :ok , key }
93
114
94
- def user_key ( alg , _ ) do
95
- raise KeyError , { :unsupported_algorithm , alg }
96
- end
115
+ { :error , :key_decode_failed } ->
116
+ message =
117
+ "unable to decode key, possibly because the key type does not support a passphrase"
97
118
98
- defp decode_pem_entry ( nil , _phrase ) do
99
- raise KeyError , { :unsupported_algorithm , :unknown }
100
- end
119
+ Logger . warn ( message )
120
+ { :error , :key_decode_failed }
101
121
102
- defp decode_pem_entry ( { _type , _data , :not_encrypted } = entry , _ ) do
103
- { :ok , :public_key . pem_entry_decode ( entry ) }
104
- end
122
+ other ->
123
+ Logger . warn ( "Unexpected return value from :ssh_file.decode/2 #{ inspect ( other ) } " )
124
+ { :error , :ssh_client_key_api_unable_to_decode_key }
125
+ end
105
126
106
- defp decode_pem_entry ( { _type , _data , { alg , _ } } , nil ) do
107
- raise KeyError , { :passphrase_required , alg }
108
- end
127
+ { _type , _data , :not_encrypted } = entry ->
128
+ result = :public_key . pem_entry_decode ( entry )
109
129
110
- defp decode_pem_entry ( { _type , _data , { alg , _ } } = entry , phrase ) do
111
- { :ok , :public_key . pem_entry_decode ( entry , phrase ) }
130
+ { :ok , result }
131
+
132
+ { _type , _data , { _alg , _ } } = entry ->
133
+ result = :public_key . pem_entry_decode ( entry , passphrase ( opts ) )
134
+ { :ok , result }
135
+
136
+ error ->
137
+ Logger . warn ( "Unexpected return value from :public_key.decode/2 #{ inspect ( error ) } " )
138
+ { :error , :ssh_client_key_api_unable_to_decode_key }
139
+ end
112
140
rescue
113
- _e in MatchError ->
114
- # credo:disable-for-next-line Credo.Check.Warning.RaiseInsideRescue
115
- raise KeyError , { :incorrect_passphrase , alg }
141
+ e ->
142
+ Logger . warn ( "user_key exception: #{ inspect ( e ) } " )
143
+ raise e
116
144
end
117
145
118
- defp identity_data ( opts ) do
119
- cb_opts ( opts ) [ :identity_data ]
146
+ defp cb_opts ( opts ) do
147
+ opts [ :key_cb_private ]
120
148
end
121
149
122
- defp silently_accept_hosts ( opts ) do
123
- cb_opts ( opts ) [ :silently_accept_hosts ]
150
+ defp known_hosts_data ( opts ) do
151
+ cb_opts ( opts ) [ :known_hosts_data ]
124
152
end
125
153
126
154
defp known_hosts ( opts ) do
127
155
cb_opts ( opts ) [ :known_hosts ]
128
156
end
129
157
130
- defp known_hosts_data ( opts ) do
131
- cb_opts ( opts ) [ :known_hosts_data ]
158
+ defp identity_data ( opts ) do
159
+ cb_opts ( opts ) [ :identity_data ]
132
160
end
133
161
134
162
defp passphrase ( opts ) do
135
163
cb_opts ( opts ) [ :passphrase ]
136
- end
164
+ |> case do
165
+ # Needs to be a charlist
166
+ passphrase when is_list ( passphrase ) ->
167
+ passphrase
137
168
138
- defp cb_opts ( opts ) do
139
- opts [ :key_cb_private ]
140
- end
169
+ passphrase when is_binary ( passphrase ) ->
170
+ Logger . warn ( "Passphrase must be a charlist, not a binary. Ignoring." )
171
+ nil
141
172
142
- defp has_fingerprint ( fingerprints , key , hostname ) do
143
- Enum . any? ( fingerprints , fn { k , v } -> k == key && Enum . member? ( v [ :hostnames ] , hostname ) end )
173
+ nil ->
174
+ nil
175
+ end
144
176
end
145
177
178
+ # Handles the case where the ype of hostname is
179
+ # `[inet:ip_address() | inet:hostname()]`
180
+ defp normalize_hostname ( [ hostname , _ip_addr ] ) , do: hostname
181
+ defp normalize_hostname ( hostname ) , do: hostname
182
+
146
183
defp default_user_dir , do: Path . join ( System . user_home! ( ) , ".ssh" )
147
184
148
185
defp default_identity do
0 commit comments