%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

%%

%%% Description: SSH file handling

-module(ssh_file).

-behaviour(ssh_server_key_api).
-behaviour(ssh_client_key_api).

-include_lib("public_key/include/public_key.hrl").
-include_lib("kernel/include/file.hrl").

-include("ssh.hrl").

-export([host_key/2,
	 user_key/2,
	 is_host_key/4,
	 add_host_key/3,
	 is_auth_key/3]).


-export_type([system_dir_daemon_option/0,
              user_dir_common_option/0,
              user_dir_fun_common_option/0,
              pubkey_passphrase_client_options/0
             ]).

-type system_dir_daemon_option()   :: {system_dir, string()}.
-type user_dir_common_option()     :: {user_dir,  string()}.
-type user_dir_fun_common_option() :: {user_dir_fun, user2dir()}.
-type user2dir() :: fun((RemoteUserName::string()) -> UserDir :: string()) .

-type pubkey_passphrase_client_options() ::   {dsa_pass_phrase,      string()}
                                            | {rsa_pass_phrase,      string()}
%% Not yet implemented:                     | {ed25519_pass_phrase,  string()}
%% Not yet implemented:                     | {ed448_pass_phrase,    string()}
                                            | {ecdsa_pass_phrase,    string()} .


-define(PERM_700, 8#700).
-define(PERM_644, 8#644).


%%% API

%% Used by server
host_key(Algorithm, Opts) ->
    File = file_name(system, file_base_name(Algorithm), Opts),
    %% We do not expect host keys to have pass phrases
    %% so probably we could hardcod Password = ignore, but
    %% we keep it as an undocumented option for now.
    Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore),
    case decode(File, Password) of
	{ok,Key} ->
            check_key_type(Key, Algorithm);
	{error,DecodeError} ->
            {error,DecodeError}
    end.

is_auth_key(Key, User,Opts) ->
    case lookup_user_key(Key, User, Opts) of
	{ok, Key} ->
	    true;
	_ ->
	    false
    end.


%% Used by client
is_host_key(Key, PeerName, Algorithm, Opts) ->
    case lookup_host_key(Key, PeerName, Algorithm, Opts) of
	{ok, Key} ->
	    true;
	_ ->
	    false
    end.

user_key(Algorithm, Opts) ->
    File = file_name(user, identity_key_filename(Algorithm), Opts),
    Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore),
    case decode(File, Password) of
        {ok, Key} ->
            check_key_type(Key, Algorithm);
        Error ->
            Error
    end.


%% Internal functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
check_key_type(Key, Algorithm) ->
    case ssh_transport:valid_key_sha_alg(Key,Algorithm) of
        true -> {ok,Key};
        false -> {error,bad_keytype_in_file}
    end.

file_base_name('ssh-rsa'            ) -> "ssh_host_rsa_key";
file_base_name('rsa-sha2-256'       ) -> "ssh_host_rsa_key";
file_base_name('rsa-sha2-384'       ) -> "ssh_host_rsa_key";
file_base_name('rsa-sha2-512'       ) -> "ssh_host_rsa_key";
file_base_name('ssh-dss'            ) -> "ssh_host_dsa_key";
file_base_name('ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key";
file_base_name('ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key";
file_base_name('ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key";
file_base_name('ssh-ed25519'        ) -> "ssh_host_ed25519_key";
file_base_name('ssh-ed448'          ) -> "ssh_host_ed448_key";
file_base_name(_                    ) -> "ssh_host_key".

decode(File, Password) ->
    try {ok, decode_ssh_file(read_ssh_file(File), Password)}
    catch 
	throw:Reason ->
	    {error, Reason};
	error:Reason ->
	    {error, Reason}
    end.      

read_ssh_file(File) ->
    {ok, Bin} = file:read_file(File),
    Bin.

%% Public key
decode_ssh_file(SshBin, public_key) ->
    public_key:ssh_decode(SshBin, public_key);

%% Private Key
decode_ssh_file(Pem, Password) ->
    case public_key:pem_decode(Pem) of
	[{_, _, not_encrypted} = Entry]  -> 
	    public_key:pem_entry_decode(Entry);
	[Entry] when Password =/= ignore ->
	    public_key:pem_entry_decode(Entry, Password);
	 _ ->
	    throw("No pass phrase provided for private key file")
    end.
    

%% lookup_host_key
%% return {ok, Key(s)} or {error, not_found}
%%

lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
    Host1 = replace_localhost(Host),
    do_lookup_host_key(KeyToMatch, Host1, Alg, Opts).
	    

add_host_key(Host, Key, Opts) ->
    Host1 = add_ip(replace_localhost(Host)),
    KnownHosts = file_name(user, "known_hosts", Opts),
    case file:open(KnownHosts, [write,append]) of
   	{ok, Fd} ->
	    ok = file:change_mode(KnownHosts, ?PERM_644),
   	    Res = add_key_fd(Fd, Host1, Key),
   	    file:close(Fd),
   	    Res;
   	Error ->
   	    Error
    end.

lookup_user_key(Key, User, Opts) ->
    SshDir = ssh_dir({remoteuser,User}, Opts),
    case lookup_user_key_f(Key, User, SshDir, "authorized_keys", Opts) of
	{ok, Key} ->
	    {ok, Key};
	_ ->
	    lookup_user_key_f(Key, User, SshDir, "authorized_keys2", Opts)
    end.


%%
%% Utils
%%

%% server use this to find individual keys for
%% an individual user when user tries to login
%% with publickey
ssh_dir({remoteuser, User}, Opts) ->
    case proplists:get_value(user_dir_fun, Opts) of
	undefined ->
	    case proplists:get_value(user_dir, Opts, false) of
		false ->
		    default_user_dir();
		Dir ->
		    Dir
	    end;
	FUN ->
	    FUN(User)
    end;

%% client use this to find client ssh keys
ssh_dir(user, Opts) ->
    case proplists:get_value(user_dir, Opts, false) of
	false -> default_user_dir();
	D -> D
    end;

%% server use this to find server host keys
ssh_dir(system, Opts) ->
    proplists:get_value(system_dir, Opts, "/etc/ssh").


file_name(Type, Name, Opts) ->
    FN = filename:join(ssh_dir(Type, Opts), Name),
    FN.



%% in: "host" out: "host,1.2.3.4.
add_ip(IP) when is_tuple(IP) ->
    ssh_connection:encode_ip(IP);
add_ip(Host)                                                             ->
    case inet:getaddr(Host, inet) of
	{ok, Addr} ->
	    case ssh_connection:encode_ip(Addr) of
		false -> Host;
		IPString -> Host ++ "," ++ IPString
	    end;
	_ -> Host
    end.    

replace_localhost("localhost") ->
    {ok, Hostname} = inet:gethostname(),
    Hostname;
replace_localhost(Host) ->
    Host.

do_lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
    case file:open(file_name(user, "known_hosts", Opts), [read, binary]) of
	{ok, Fd} ->
	    Res = lookup_host_key_fd(Fd, KeyToMatch, Host, Alg),
	    file:close(Fd),
	    Res;
	{error, enoent} ->
	    {error, not_found};
	Error ->
	    Error
    end.

identity_key_filename('ssh-dss'            ) -> "id_dsa";
identity_key_filename('ssh-rsa'            ) -> "id_rsa";
identity_key_filename('rsa-sha2-256'       ) -> "id_rsa";
identity_key_filename('rsa-sha2-384'       ) -> "id_rsa";
identity_key_filename('rsa-sha2-512'       ) -> "id_rsa";
identity_key_filename('ssh-ed25519'        ) -> "id_ed25519";
identity_key_filename('ssh-ed448'          ) -> "id_ed448";
identity_key_filename('ecdsa-sha2-nistp256') -> "id_ecdsa";
identity_key_filename('ecdsa-sha2-nistp384') -> "id_ecdsa";
identity_key_filename('ecdsa-sha2-nistp521') -> "id_ecdsa".

identity_pass_phrase("ssh-dss"       ) -> dsa_pass_phrase;
identity_pass_phrase("ssh-rsa"       ) -> rsa_pass_phrase;
identity_pass_phrase("rsa-sha2-256"  ) -> rsa_pass_phrase;
identity_pass_phrase("rsa-sha2-384"  ) -> rsa_pass_phrase;
identity_pass_phrase("rsa-sha2-512"  ) -> rsa_pass_phrase;
%% Not yet implemented: identity_pass_phrase("ssh-ed25519"   ) -> ed25519_pass_phrase;
%% Not yet implemented: identity_pass_phrase("ssh-ed448"     ) -> ed448_pass_phrase;
identity_pass_phrase("ecdsa-sha2-"++_) -> ecdsa_pass_phrase;
identity_pass_phrase(P) when is_atom(P) -> 
    identity_pass_phrase(atom_to_list(P));
identity_pass_phrase(_) -> undefined.
    
lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType) ->
    case io:get_line(Fd, '') of
	eof ->
	    {error, not_found};
	{error,Error} ->
	    %% Rare... For example NFS errors
	    {error,Error};
	Line ->
	    case ssh_decode_line(Line, known_hosts) of
		[{Key, Attributes}] ->
		    handle_host(Fd, KeyToMatch, Host, proplists:get_value(hostnames, Attributes), Key, KeyType);
		[] ->
		    lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
	    end
    end.

ssh_decode_line(Line, Type) ->
    try 
	public_key:ssh_decode(Line, Type) 
    catch _:_ ->
	    []
    end.

handle_host(Fd, KeyToMatch, Host, HostList, Key, KeyType) ->
    Host1 = host_name(Host),
    case lists:member(Host1, HostList) andalso key_match(Key, KeyType) of
	true when KeyToMatch == Key ->
	    {ok,Key};
	_ ->
	    lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
    end.

host_name(Atom) when is_atom(Atom) ->
    atom_to_list(Atom);
host_name(List) ->
    List.

key_match(#'RSAPublicKey'{}, 'ssh-rsa') ->
    true;
key_match({_, #'Dss-Parms'{}}, 'ssh-dss') ->
    true;
key_match({#'ECPoint'{},{namedCurve,Curve}}, Alg) ->
    case atom_to_list(Alg) of
	"ecdsa-sha2-"++IdS ->
	    Curve == public_key:ssh_curvename2oid(list_to_binary(IdS));
	_ ->
	    false
    end;
key_match({ed_pub,ed25519,_}, 'ssh-ed25519') ->
    true;
key_match({ed_pub,ed448,_}, 'ssh-ed448') ->
    true;
key_match(_, _) ->
    false.

add_key_fd(Fd, Host,Key) ->
    SshBin = public_key:ssh_encode([{Key, [{hostnames, [Host]}]}], known_hosts),
    file:write(Fd, SshBin).

lookup_user_key_f(_, _User, [], _F, _Opts) ->
    {error, nouserdir};
lookup_user_key_f(_, _User, nouserdir, _F, _Opts) ->
    {error, nouserdir};
lookup_user_key_f(Key, _User, Dir, F, _Opts) ->
    FileName = filename:join(Dir, F),
    case file:open(FileName, [read, binary]) of
	{ok, Fd} ->
	    Res = lookup_user_key_fd(Fd, Key),
	    file:close(Fd),
	    Res;
	{error, Reason} ->
	    {error, {{openerr, Reason}, {file, FileName}}}
    end.

lookup_user_key_fd(Fd, Key) ->
    case io:get_line(Fd, '') of
	eof ->
	    {error, not_found};
	{error,Error} ->
	    %% Rare... For example NFS errors
	    {error,Error};
	Line ->
	    case ssh_decode_line(Line, auth_keys) of
		[{AuthKey, _}] ->
		    case is_auth_key(Key, AuthKey) of
			true ->
			    {ok, Key};
			false ->
			    lookup_user_key_fd(Fd, Key)
		    end;
		[] ->
		    lookup_user_key_fd(Fd, Key)
	    end
    end.

is_auth_key(Key, Key) -> 
    true;
is_auth_key(_,_) -> 
    false.


default_user_dir() ->
    try
	default_user_dir(os:getenv("HOME"))
    catch
	_:_ ->
	    default_user_dir(init:get_argument(home))
    end.

default_user_dir({ok,[[Home|_]]}) ->
    default_user_dir(Home);
default_user_dir(Home) when is_list(Home) ->
    UserDir = filename:join(Home, ".ssh"),
    ok = filelib:ensure_dir(filename:join(UserDir, "dummy")),
    {ok,Info} = file:read_file_info(UserDir),
    #file_info{mode=Mode} = Info,
    case (Mode band 8#777) of
	?PERM_700 ->
	    ok;
	_Other ->
	    ok = file:change_mode(UserDir, ?PERM_700)
    end,
    UserDir.
