1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22
23%%% Description: SSH file handling
24
25-module(ssh_file).
26
27-behaviour(ssh_server_key_api).
28-behaviour(ssh_client_key_api).
29
30-include_lib("public_key/include/public_key.hrl").
31-include_lib("kernel/include/file.hrl").
32
33-include("ssh.hrl").
34
35-export([host_key/2,
36	 user_key/2,
37	 is_host_key/4,
38	 add_host_key/3,
39	 is_auth_key/3]).
40
41
42-export_type([system_dir_daemon_option/0,
43              user_dir_common_option/0,
44              user_dir_fun_common_option/0,
45              pubkey_passphrase_client_options/0
46             ]).
47
48-type system_dir_daemon_option()   :: {system_dir, string()}.
49-type user_dir_common_option()     :: {user_dir,  string()}.
50-type user_dir_fun_common_option() :: {user_dir_fun, user2dir()}.
51-type user2dir() :: fun((RemoteUserName::string()) -> UserDir :: string()) .
52
53-type pubkey_passphrase_client_options() ::   {dsa_pass_phrase,      string()}
54                                            | {rsa_pass_phrase,      string()}
55%% Not yet implemented:                     | {ed25519_pass_phrase,  string()}
56%% Not yet implemented:                     | {ed448_pass_phrase,    string()}
57                                            | {ecdsa_pass_phrase,    string()} .
58
59
60-define(PERM_700, 8#700).
61-define(PERM_644, 8#644).
62
63
64%%% API
65
66%% Used by server
67host_key(Algorithm, Opts) ->
68    File = file_name(system, file_base_name(Algorithm), Opts),
69    %% We do not expect host keys to have pass phrases
70    %% so probably we could hardcod Password = ignore, but
71    %% we keep it as an undocumented option for now.
72    Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore),
73    case decode(File, Password) of
74	{ok,Key} ->
75            check_key_type(Key, Algorithm);
76	{error,DecodeError} ->
77            {error,DecodeError}
78    end.
79
80is_auth_key(Key, User,Opts) ->
81    case lookup_user_key(Key, User, Opts) of
82	{ok, Key} ->
83	    true;
84	_ ->
85	    false
86    end.
87
88
89%% Used by client
90is_host_key(Key, PeerName, Algorithm, Opts) ->
91    case lookup_host_key(Key, PeerName, Algorithm, Opts) of
92	{ok, Key} ->
93	    true;
94	_ ->
95	    false
96    end.
97
98user_key(Algorithm, Opts) ->
99    File = file_name(user, identity_key_filename(Algorithm), Opts),
100    Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore),
101    case decode(File, Password) of
102        {ok, Key} ->
103            check_key_type(Key, Algorithm);
104        Error ->
105            Error
106    end.
107
108
109%% Internal functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
110check_key_type(Key, Algorithm) ->
111    case ssh_transport:valid_key_sha_alg(Key,Algorithm) of
112        true -> {ok,Key};
113        false -> {error,bad_keytype_in_file}
114    end.
115
116file_base_name('ssh-rsa'            ) -> "ssh_host_rsa_key";
117file_base_name('rsa-sha2-256'       ) -> "ssh_host_rsa_key";
118file_base_name('rsa-sha2-384'       ) -> "ssh_host_rsa_key";
119file_base_name('rsa-sha2-512'       ) -> "ssh_host_rsa_key";
120file_base_name('ssh-dss'            ) -> "ssh_host_dsa_key";
121file_base_name('ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key";
122file_base_name('ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key";
123file_base_name('ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key";
124file_base_name('ssh-ed25519'        ) -> "ssh_host_ed25519_key";
125file_base_name('ssh-ed448'          ) -> "ssh_host_ed448_key";
126file_base_name(_                    ) -> "ssh_host_key".
127
128decode(File, Password) ->
129    try {ok, decode_ssh_file(read_ssh_file(File), Password)}
130    catch
131	throw:Reason ->
132	    {error, Reason};
133	error:Reason ->
134	    {error, Reason}
135    end.
136
137read_ssh_file(File) ->
138    {ok, Bin} = file:read_file(File),
139    Bin.
140
141%% Public key
142decode_ssh_file(SshBin, public_key) ->
143    public_key:ssh_decode(SshBin, public_key);
144
145%% Private Key
146decode_ssh_file(Pem, Password) ->
147    case public_key:pem_decode(Pem) of
148	[{_, _, not_encrypted} = Entry]  ->
149	    public_key:pem_entry_decode(Entry);
150	[Entry] when Password =/= ignore ->
151	    public_key:pem_entry_decode(Entry, Password);
152	 _ ->
153	    throw("No pass phrase provided for private key file")
154    end.
155
156
157%% lookup_host_key
158%% return {ok, Key(s)} or {error, not_found}
159%%
160
161lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
162    Host1 = replace_localhost(Host),
163    do_lookup_host_key(KeyToMatch, Host1, Alg, Opts).
164
165
166add_host_key(Host, Key, Opts) ->
167    Host1 = add_ip(replace_localhost(Host)),
168    KnownHosts = file_name(user, "known_hosts", Opts),
169    case file:open(KnownHosts, [write,append]) of
170   	{ok, Fd} ->
171	    ok = file:change_mode(KnownHosts, ?PERM_644),
172   	    Res = add_key_fd(Fd, Host1, Key),
173   	    file:close(Fd),
174   	    Res;
175   	Error ->
176   	    Error
177    end.
178
179lookup_user_key(Key, User, Opts) ->
180    SshDir = ssh_dir({remoteuser,User}, Opts),
181    case lookup_user_key_f(Key, User, SshDir, "authorized_keys", Opts) of
182	{ok, Key} ->
183	    {ok, Key};
184	_ ->
185	    lookup_user_key_f(Key, User, SshDir, "authorized_keys2", Opts)
186    end.
187
188
189%%
190%% Utils
191%%
192
193%% server use this to find individual keys for
194%% an individual user when user tries to login
195%% with publickey
196ssh_dir({remoteuser, User}, Opts) ->
197    case proplists:get_value(user_dir_fun, Opts) of
198	undefined ->
199	    case proplists:get_value(user_dir, Opts, false) of
200		false ->
201		    default_user_dir();
202		Dir ->
203		    Dir
204	    end;
205	FUN ->
206	    FUN(User)
207    end;
208
209%% client use this to find client ssh keys
210ssh_dir(user, Opts) ->
211    case proplists:get_value(user_dir, Opts, false) of
212	false -> default_user_dir();
213	D -> D
214    end;
215
216%% server use this to find server host keys
217ssh_dir(system, Opts) ->
218    proplists:get_value(system_dir, Opts, "/etc/ssh").
219
220
221file_name(Type, Name, Opts) ->
222    FN = filename:join(ssh_dir(Type, Opts), Name),
223    FN.
224
225
226
227%% in: "host" out: "host,1.2.3.4.
228add_ip(IP) when is_tuple(IP) ->
229    ssh_connection:encode_ip(IP);
230add_ip(Host)                                                             ->
231    case inet:getaddr(Host, inet) of
232	{ok, Addr} ->
233	    case ssh_connection:encode_ip(Addr) of
234		false -> Host;
235		IPString -> Host ++ "," ++ IPString
236	    end;
237	_ -> Host
238    end.
239
240replace_localhost("localhost") ->
241    {ok, Hostname} = inet:gethostname(),
242    Hostname;
243replace_localhost(Host) ->
244    Host.
245
246do_lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
247    case file:open(file_name(user, "known_hosts", Opts), [read, binary]) of
248	{ok, Fd} ->
249	    Res = lookup_host_key_fd(Fd, KeyToMatch, Host, Alg),
250	    file:close(Fd),
251	    Res;
252	{error, enoent} ->
253	    {error, not_found};
254	Error ->
255	    Error
256    end.
257
258identity_key_filename('ssh-dss'            ) -> "id_dsa";
259identity_key_filename('ssh-rsa'            ) -> "id_rsa";
260identity_key_filename('rsa-sha2-256'       ) -> "id_rsa";
261identity_key_filename('rsa-sha2-384'       ) -> "id_rsa";
262identity_key_filename('rsa-sha2-512'       ) -> "id_rsa";
263identity_key_filename('ssh-ed25519'        ) -> "id_ed25519";
264identity_key_filename('ssh-ed448'          ) -> "id_ed448";
265identity_key_filename('ecdsa-sha2-nistp256') -> "id_ecdsa";
266identity_key_filename('ecdsa-sha2-nistp384') -> "id_ecdsa";
267identity_key_filename('ecdsa-sha2-nistp521') -> "id_ecdsa".
268
269identity_pass_phrase("ssh-dss"       ) -> dsa_pass_phrase;
270identity_pass_phrase("ssh-rsa"       ) -> rsa_pass_phrase;
271identity_pass_phrase("rsa-sha2-256"  ) -> rsa_pass_phrase;
272identity_pass_phrase("rsa-sha2-384"  ) -> rsa_pass_phrase;
273identity_pass_phrase("rsa-sha2-512"  ) -> rsa_pass_phrase;
274%% Not yet implemented: identity_pass_phrase("ssh-ed25519"   ) -> ed25519_pass_phrase;
275%% Not yet implemented: identity_pass_phrase("ssh-ed448"     ) -> ed448_pass_phrase;
276identity_pass_phrase("ecdsa-sha2-"++_) -> ecdsa_pass_phrase;
277identity_pass_phrase(P) when is_atom(P) ->
278    identity_pass_phrase(atom_to_list(P));
279identity_pass_phrase(_) -> undefined.
280
281lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType) ->
282    case io:get_line(Fd, '') of
283	eof ->
284	    {error, not_found};
285	{error,Error} ->
286	    %% Rare... For example NFS errors
287	    {error,Error};
288	Line ->
289	    case ssh_decode_line(Line, known_hosts) of
290		[{Key, Attributes}] ->
291		    handle_host(Fd, KeyToMatch, Host, proplists:get_value(hostnames, Attributes), Key, KeyType);
292		[] ->
293		    lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
294	    end
295    end.
296
297ssh_decode_line(Line, Type) ->
298    try
299	public_key:ssh_decode(Line, Type)
300    catch _:_ ->
301	    []
302    end.
303
304handle_host(Fd, KeyToMatch, Host, HostList, Key, KeyType) ->
305    Host1 = host_name(Host),
306    case lists:member(Host1, HostList) andalso key_match(Key, KeyType) of
307	true when KeyToMatch == Key ->
308	    {ok,Key};
309	_ ->
310	    lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
311    end.
312
313host_name(Atom) when is_atom(Atom) ->
314    atom_to_list(Atom);
315host_name(List) ->
316    List.
317
318key_match(#'RSAPublicKey'{}, 'ssh-rsa') ->
319    true;
320key_match({_, #'Dss-Parms'{}}, 'ssh-dss') ->
321    true;
322key_match({#'ECPoint'{},{namedCurve,Curve}}, Alg) ->
323    case atom_to_list(Alg) of
324	"ecdsa-sha2-"++IdS ->
325	    Curve == public_key:ssh_curvename2oid(list_to_binary(IdS));
326	_ ->
327	    false
328    end;
329key_match({ed_pub,ed25519,_}, 'ssh-ed25519') ->
330    true;
331key_match({ed_pub,ed448,_}, 'ssh-ed448') ->
332    true;
333key_match(_, _) ->
334    false.
335
336add_key_fd(Fd, Host,Key) ->
337    SshBin = public_key:ssh_encode([{Key, [{hostnames, [Host]}]}], known_hosts),
338    file:write(Fd, SshBin).
339
340lookup_user_key_f(_, _User, [], _F, _Opts) ->
341    {error, nouserdir};
342lookup_user_key_f(_, _User, nouserdir, _F, _Opts) ->
343    {error, nouserdir};
344lookup_user_key_f(Key, _User, Dir, F, _Opts) ->
345    FileName = filename:join(Dir, F),
346    case file:open(FileName, [read, binary]) of
347	{ok, Fd} ->
348	    Res = lookup_user_key_fd(Fd, Key),
349	    file:close(Fd),
350	    Res;
351	{error, Reason} ->
352	    {error, {{openerr, Reason}, {file, FileName}}}
353    end.
354
355lookup_user_key_fd(Fd, Key) ->
356    case io:get_line(Fd, '') of
357	eof ->
358	    {error, not_found};
359	{error,Error} ->
360	    %% Rare... For example NFS errors
361	    {error,Error};
362	Line ->
363	    case ssh_decode_line(Line, auth_keys) of
364		[{AuthKey, _}] ->
365		    case is_auth_key(Key, AuthKey) of
366			true ->
367			    {ok, Key};
368			false ->
369			    lookup_user_key_fd(Fd, Key)
370		    end;
371		[] ->
372		    lookup_user_key_fd(Fd, Key)
373	    end
374    end.
375
376is_auth_key(Key, Key) ->
377    true;
378is_auth_key(_,_) ->
379    false.
380
381
382default_user_dir() ->
383    try
384	default_user_dir(os:getenv("HOME"))
385    catch
386	_:_ ->
387	    default_user_dir(init:get_argument(home))
388    end.
389
390default_user_dir({ok,[[Home|_]]}) ->
391    default_user_dir(Home);
392default_user_dir(Home) when is_list(Home) ->
393    UserDir = filename:join(Home, ".ssh"),
394    ok = filelib:ensure_dir(filename:join(UserDir, "dummy")),
395    {ok,Info} = file:read_file_info(UserDir),
396    #file_info{mode=Mode} = Info,
397    case (Mode band 8#777) of
398	?PERM_700 ->
399	    ok;
400	_Other ->
401	    ok = file:change_mode(UserDir, ?PERM_700)
402    end,
403    UserDir.
404