aboutsummaryrefslogblamecommitdiffstats
path: root/lib/ssh/src/ssh_file.erl
blob: c78f5dc33777f759a923bf5b99a24b9dd682da02 (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                   


                                                        




                                                                      
  



                                                                         
  












                                  

                                        















                                                             


                         





































































                                                                           

                                                      
                   
                                                         


















































































                                                                           










                                                                                       


































































































































































































































































































































                                                                                                    



                                                             
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2010. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%

%%

%%% Description: SSH file handling

-module(ssh_file).

-include("ssh.hrl").
-include("PKCS-1.hrl").
-include("DSS.hrl").

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

-export([public_host_dsa_key/2,private_host_dsa_key/2,
	 public_host_rsa_key/2,private_host_rsa_key/2,
	 public_host_key/2,private_host_key/2,
	 lookup_host_key/3, add_host_key/3, % del_host_key/2,
	 lookup_user_key/3, ssh_dir/2, file_name/3]).

-export([private_identity_key/2]).
%% , public_identity_key/2,
%% 	 identity_keys/2]).

-export([encode_public_key/1, decode_public_key_v2/2]).

-import(lists, [reverse/1, append/1]).

-define(DBG_PATHS, true).

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

%% API
public_host_dsa_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_dsa_key.pub", Opts),
    read_public_key_v2(File, "ssh-dss").

private_host_dsa_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_dsa_key", Opts),
    read_private_key_v2(File, "ssh-dss").

public_host_rsa_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_rsa_key.pub", Opts),
    read_public_key_v2(File, "ssh-rsa").

private_host_rsa_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_rsa_key", Opts),
    read_private_key_v2(File, "ssh-rsa").

public_host_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_key", Opts),
    case read_private_key_v1(File,public) of
	{error, enoent} ->	
	    read_public_key_v1(File++".pub");
	Result ->
	    Result
    end.
	    

private_host_key(Type, Opts) ->
    File = file_name(Type, "ssh_host_key", Opts),
    read_private_key_v1(File,private).



%% in: "host" out: "host,1.2.3.4.
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.

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

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

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.

%% del_host_key(Host, Opts) ->
%%     Host1 = replace_localhost(Host),
%%     case file:open(file_name(user, "known_hosts", Opts),[write,read]) of
%% 	{ok, Fd} ->
%% 	    Res = del_key_fd(Fd, Host1),
%% 	    file:close(Fd),
%% 	    Res;
%% 	Error ->
%% 	    Error
%%     end.

identity_key_filename("ssh-dss") -> "id_dsa";
identity_key_filename("ssh-rsa") -> "id_rsa".

private_identity_key(Alg, Opts) ->
    Path = file_name(user, identity_key_filename(Alg), Opts),
    read_private_key_v2(Path, Alg).

read_public_key_v2(File, Type) ->
    case file:read_file(File) of
	{ok,Bin} ->
	    List = binary_to_list(Bin),
	    case lists:prefix(Type, List) of
		true ->
		    List1 = lists:nthtail(length(Type), List),
		    K_S = ssh_bits:b64_decode(List1),
		    decode_public_key_v2(K_S, Type);
		false ->
		    {error, bad_format}
	    end;
	Error ->
	    Error
    end.

decode_public_key_v2(K_S, "ssh-rsa") ->
    case ssh_bits:decode(K_S,[string,mpint,mpint]) of
	["ssh-rsa", E, N] ->
	    {ok, #ssh_key { type = rsa,
			    public = {N,E},
			    comment=""}};
	_ ->
	    {error, bad_format}
    end;
decode_public_key_v2(K_S, "ssh-dss") ->
    case ssh_bits:decode(K_S,[string,mpint,mpint,mpint,mpint]) of
	["ssh-dss",P,Q,G,Y] ->
	    {ok,#ssh_key { type = dsa,
			   public = {P,Q,G,Y}
			  }};
	_A ->
	    {error, bad_format}
    end;
decode_public_key_v2(_, _) ->
    {error, bad_format}.
    

read_public_key_v1(File) ->
    case file:read_file(File) of
	{ok,Bin} ->
	    List = binary_to_list(Bin),
	    case io_lib:fread("~d ~d ~d ~s", List) of
		{ok,[_Sz,E,N,Comment],_} ->
		    {ok,#ssh_key { type = rsa,
				   public ={N,E},
				   comment = Comment }};
		_Error ->
		    {error, bad_format}
	    end;
	Error ->
	    Error
    end.

%% pem_type("ssh-dss") -> "DSA";
%% pem_type("ssh-rsa") -> "RSA".

read_private_key_v2(File, Type) ->
    case file:read_file(File) of
        {ok, PemBin} ->
            case catch (public_key:pem_decode(PemBin)) of
                [{_, Bin, not_encrypted}] ->
                    decode_private_key_v2(Bin, Type);
                Error -> %% Note we do not handle password encrypted keys at the moment
                    {error, Error}
            end;
        {error, Reason} ->
            {error, Reason}
    end.
%%  case file:read_file(File) of
%% 	{ok,Bin} ->
%% 	    case read_pem(binary_to_list(Bin), pem_type(Type)) of
%% 		{ok,Bin1} ->
%% 		    decode_private_key_v2(Bin1, Type);
%% 		Error ->
%% 		    Error
%% 	    end;
%% 	Error ->
%% 	    Error
%%     end.

decode_private_key_v2(Private,"ssh-rsa") ->
    case 'PKCS-1':decode( 'RSAPrivateKey', Private) of
	{ok,RSA} -> %% FIXME Check for two-prime version
	    {ok, #ssh_key { type = rsa,
			    public = {RSA#'RSAPrivateKey'.modulus,
				      RSA#'RSAPrivateKey'.publicExponent},
			    private = {RSA#'RSAPrivateKey'.modulus,
				       RSA#'RSAPrivateKey'.privateExponent}
			    }};
	Error ->
	    Error
    end;
decode_private_key_v2(Private, "ssh-dss") ->
    case 'DSS':decode('DSAPrivateKey', Private) of
	{ok,DSA} -> %% FIXME Check for two-prime version
	    {ok, #ssh_key { type = dsa,
			    public = {DSA#'DSAPrivateKey'.p,
				      DSA#'DSAPrivateKey'.q,
				      DSA#'DSAPrivateKey'.g,
				      DSA#'DSAPrivateKey'.y},
			    private= {DSA#'DSAPrivateKey'.p,
				      DSA#'DSAPrivateKey'.q,
				      DSA#'DSAPrivateKey'.g,
				      DSA#'DSAPrivateKey'.x}
			   }};
	_ ->
	    {error,bad_format}
    end.

%% SSH1 private key format
%%  <<"SSH PRIVATE KEY FILE FORMATE 1.1\n" 0:8 
%%    CipherNum:8, Reserved:32,
%%    NSz/uint32, N/bignum, E/bignum, Comment/string,
%%
%% [ R0:8 R1:8 R0:8 R1:8, D/bignum, IQMP/bignum, Q/bignum, P/bignum, Pad(8)]>>
%%
%% where [ ] is encrypted using des3 (ssh1 version) and
%% a posssibly empty pass phrase using md5(passphase) as key
%% 

read_private_key_v1(File, Type) ->
    case file:read_file(File) of
	{ok,<<"SSH PRIVATE KEY FILE FORMAT 1.1\n",0,
	     CipherNum,_Resereved:32,Bin/binary>>} ->
	    decode_private_key_v1(Bin, CipherNum,Type);
	{ok,_} ->
	    {error, bad_format};
	Error ->
	    Error
    end.

decode_private_key_v1(Bin, CipherNum, Type) ->
    case ssh_bits:decode(Bin,0,[uint32, bignum, bignum, string]) of
	{Offset,[_NSz,N,E,Comment]} ->
	    if Type == public ->
		    {ok,#ssh_key { type=rsa,
				   public={N,E},
				   comment=Comment}};
	       Type == private ->
		    <<_:Offset/binary, Encrypted/binary>> = Bin,
		    case ssh_bits:decode(decrypt1(Encrypted, CipherNum),0,
					 [uint32, bignum, bignum, 
					  bignum, bignum,{pad,8}]) of
			{_,[_,D,IQMP,Q,P]} ->
			    {ok,#ssh_key { type=rsa,
					   public={N,E},
					   private={D,IQMP,Q,P},
					   comment=Comment}};
			_ ->
			    {error,bad_format}
		    end
	    end;
	_ ->
	    {error,bad_format}
    end.


decrypt1(Bin, CipherNum) ->
    decrypt1(Bin, CipherNum,"").

decrypt1(Bin, CipherNum, Phrase) ->
    if CipherNum == ?SSH_CIPHER_NONE; Phrase == "" ->
	    Bin;
       CipherNum == ?SSH_CIPHER_3DES ->
	    <<K1:8/binary, K2:8/binary>> = erlang:md5(Phrase),
	    K3 = K1,
	    IV = <<0,0,0,0,0,0,0,0>>,
	    Bin1 = crypto:des_cbc_decrypt(K3,IV,Bin),
	    Bin2 = crypto:des_cbc_encrypt(K2,IV,Bin1),
	    crypto:des_cbc_decrypt(K1,IV,Bin2)
    end.

%% encrypt1(Bin, CipherNum) ->
%%     encrypt1(Bin, CipherNum,"").

%% encrypt1(Bin, CipherNum, Phrase) ->
%%     if CipherNum == ?SSH_CIPHER_NONE; Phrase == "" ->
%% 	    Bin;
%%        CipherNum == ?SSH_CIPHER_3DES ->
%% 	    <<K1:8/binary, K2:8/binary>> = erlang:md5(Phrase),
%% 	    K3 = K1,
%% 	    IV = <<0,0,0,0,0,0,0,0>>,
%% 	    Bin1 = crypto:des_cbc_encrypt(K1,IV,Bin),
%% 	    Bin2 = crypto:des_cbc_decrypt(K2,IV,Bin1),
%% 	    crypto:des_cbc_encrypt(K3,IV,Bin2)
%%     end.

lookup_host_key_fd(Fd, Host, Alg) ->
    case io:get_line(Fd, '') of
	eof ->
	    {error, not_found};
	Line ->
	    case string:tokens(Line, " ") of
		[HostList, Alg, KeyData] ->
%% 		    io:format(" ~p lookup_host_key_fd: HostList ~p Alg ~p KeyData ~p\n",
%%			      [Host, HostList, Alg, KeyData]),
		    case lists:member(Host, string:tokens(HostList, ",")) of
			true ->
			    decode_public_key_v2(ssh_bits:b64_decode(KeyData), Alg);
			false ->
			    lookup_host_key_fd(Fd, Host, Alg)
		    end;
		_ ->
		    lookup_host_key_fd(Fd, Host, Alg)
	    end
    end.



%% del_key_fd(Fd, Host) ->
%%     del_key_fd(Fd, Host, 0, 0).

%% del_key_fd(Fd, Host, ReadPos0, WritePos0) ->
%%     case io:get_line(Fd, '') of
%% 	eof ->
%% 	    if ReadPos0 == WritePos0 ->
%% 		    ok;
%% 	       true ->
%% 		    file:truncate(Fd)
%% 	    end;
%% 	Line ->
%% 	    {ok,ReadPos1} = file:position(Fd, cur),
%% 	    case string:tokens(Line, " ") of
%% 		[HostList, _Type, _KeyData] ->
%% 		    case lists:member(Host, string:tokens(HostList, ",")) of
%% 			true ->
%% 			    del_key_fd(Fd, Host, ReadPos1, WritePos0);
%% 			false ->
%% 			    if ReadPos0 == WritePos0 ->
%% 				    del_key_fd(Fd, Host, ReadPos1, ReadPos1);
%% 			       true ->
%% 				    file:position(Fd, WritePos0),
%% 				    file:write(Fd, Line),
%% 				    {ok,WritePos1} = file:position(Fd,cur),
%% 				    del_key_fd(Fd, Host, ReadPos1, WritePos1)
%% 			    end
%% 		    end;
%% 		_ ->
%% 		    if ReadPos0 == WritePos0 ->
%% 			    del_key_fd(Fd, Host, ReadPos1, ReadPos1);
%% 		       true ->
%% 			    file:position(Fd, WritePos0),
%% 			    file:write(Fd, Line),
%% 			    {ok,WritePos1} = file:position(Fd,cur),
%% 			    del_key_fd(Fd, Host, ReadPos1, WritePos1)
%% 		    end		    
%% 	    end
%%     end.


add_key_fd(Fd, Host, Key) ->
    case Key#ssh_key.type of
	rsa ->
	    {N,E} = Key#ssh_key.public,
	    DK = ssh_bits:b64_encode(
		   ssh_bits:encode(["ssh-rsa",E,N],
				   [string,mpint,mpint])),
	    file:write(Fd, [Host, " ssh-rsa ", DK, "\n"]);
	dsa ->
	    {P,Q,G,Y} = Key#ssh_key.public,
	    DK = ssh_bits:b64_encode(
		   ssh_bits:encode(["ssh-dss",P,Q,G,Y],
				   [string,mpint,mpint,mpint,mpint])),
	    file:write(Fd, [Host, " ssh-dss ", DK, "\n"])
    end.


%% read_pem(Cs, Type) ->
%%     case read_line(Cs) of
%% 	{"-----BEGIN "++Rest,Cs1} ->
%% 	    case string:tokens(Rest, " ") of
%% 		[Type, "PRIVATE", "KEY-----"] ->
%% 		    read_pem64(Cs1, [], Type);
%% 		_ ->
%% 		    {error, bad_format}
%% 	    end;
%% 	{"",Cs1} when Cs1 =/= "" ->
%% 	    read_pem(Cs1,Type);
%% 	{_,""} ->
%% 	    {error, bad_format}
%%     end.

%% read_pem64(Cs, Acc, Type) ->
%%     case read_line(Cs) of
%% 	{"-----END "++Rest,_Cs1} ->
%% 	    case string:tokens(Rest, " ") of
%% 		[Type, "PRIVATE", "KEY-----"] ->
%% 		    {ok,ssh_bits:b64_decode(append(reverse(Acc)))};
%% 		Toks ->
%% 		    error_logger:format("ssh: TOKENS=~p\n", [Toks]),
%% 		    {error, bad_format}
%% 	    end;
%% 	{B64, Cs1} when Cs1 =/= "" ->
%% 	    read_pem64(Cs1, [B64|Acc], Type);
%% 	_What ->
%% 	    {error, bad_format}
%%     end.


%% read_line(Cs) -> read_line(Cs,[]).
%% read_line([$\r,$\n|T], Acc) -> {reverse(Acc), T};
%% read_line([$\n|T], Acc) -> {reverse(Acc), T};
%% read_line([C|T], Acc) -> read_line(T,[C|Acc]);
%% read_line([], Acc) -> {reverse(Acc),[]}.

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

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

lookup_user_key_fd(Fd, Alg) ->
    case io:get_line(Fd, '') of
	eof ->
	    {error, not_found};
	Line ->
	    case string:tokens(Line, " ") of
		[Alg, KeyData, _] ->
		    %% 		    io:format("lookup_user_key_fd: HostList ~p Alg ~p KeyData ~p\n",
		    %% 			      [HostList, Alg, KeyData]),
		    decode_public_key_v2(ssh_bits:b64_decode(KeyData), Alg);
		_Other ->
		    %%?dbg(false, "key_fd Other: ~w ~w\n", [Alg, _Other]),
		    lookup_user_key_fd(Fd, Alg)
	    end
    end.


encode_public_key(#ssh_key{type = rsa, public = {N, E}}) ->
    ssh_bits:encode(["ssh-rsa",E,N],
		    [string,mpint,mpint]);
encode_public_key(#ssh_key{type = dsa, public = {P,Q,G,Y}}) ->
    ssh_bits:encode(["ssh-dss",P,Q,G,Y],
		    [string,mpint,mpint,mpint,mpint]).

%%
%% 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) of
		undefined ->
		    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),
    %%?dbg(?DBG_PATHS, "file_name: ~p\n", [FN]),
    FN.

default_user_dir()->
    {ok,[[Home|_]]} = init:get_argument(home),
    UserDir = filename:join(Home, ".ssh"),
    ok = filelib:ensure_dir(filename:join(UserDir, "dummy")),
    ok = file:change_mode(UserDir, ?PERM_700),
    UserDir.