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