%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2011-2012. 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% %% -module(pubkey_ssh). -include("public_key.hrl"). -export([decode/2, encode/2]). -define(UINT32(X), X:32/unsigned-big-integer). %% Max encoded line length is 72, but conformance examples use 68 %% Comment from rfc 4716: "The following are some examples of public %% key files that are compliant (note that the examples all wrap %% before 72 bytes to meet IETF document requirements; however, they %% are still compliant.)" So we choose to use 68 also. -define(ENCODED_LINE_LENGTH, 68). %%==================================================================== %% Internal application API %%==================================================================== %%-------------------------------------------------------------------- -spec decode(binary(), public_key | ssh_file()) -> [{public_key(), Attributes::list()}]. %% %% Description: Decodes a ssh file-binary. %%-------------------------------------------------------------------- decode(Bin, public_key)-> case binary:match(Bin, begin_marker()) of nomatch -> openssh_decode(Bin, openssh_public_key); _ -> rfc4716_decode(Bin) end; decode(Bin, rfc4716_public_key) -> rfc4716_decode(Bin); decode(Bin, Type) -> openssh_decode(Bin, Type). %%-------------------------------------------------------------------- -spec encode([{public_key(), Attributes::list()}], ssh_file()) -> binary(). %% %% Description: Encodes a list of ssh file entries. %%-------------------------------------------------------------------- encode(Entries, Type) -> erlang:iolist_to_binary(lists:map(fun({Key, Attributes}) -> do_encode(Type, Key, Attributes) end, Entries)). %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- begin_marker() -> <<"---- BEGIN SSH2 PUBLIC KEY ----">>. end_marker() -> <<"---- END SSH2 PUBLIC KEY ----">>. rfc4716_decode(Bin) -> Lines = binary:split(Bin, <<"\n">>, [global]), do_rfc4716_decode(Lines, []). do_rfc4716_decode([<<"---- BEGIN SSH2 PUBLIC KEY ----", _/binary>> | Lines], Acc) -> do_rfc4716_decode(Lines, Acc); %% Ignore empty lines before or after begin/end - markers. do_rfc4716_decode([<<>> | Lines], Acc) -> do_rfc4716_decode(Lines, Acc); do_rfc4716_decode([], Acc) -> lists:reverse(Acc); do_rfc4716_decode(Lines, Acc) -> {Headers, PubKey, Rest} = rfc4716_decode_lines(Lines, []), case Headers of [_|_] -> do_rfc4716_decode(Rest, [{PubKey, [{headers, Headers}]} | Acc]); _ -> do_rfc4716_decode(Rest, [{PubKey, []} | Acc]) end. rfc4716_decode_lines([Line | Lines], Acc) -> case binary:last(Line) of $\\ -> NewLine = binary:replace(Line,<<"\\">>, hd(Lines), []), rfc4716_decode_lines([NewLine | tl(Lines)], Acc); _ -> rfc4716_decode_line(Line, Lines, Acc) end. rfc4716_decode_line(Line, Lines, Acc) -> case binary:split(Line, <<":">>) of [Tag, Value] -> rfc4716_decode_lines(Lines, [{string_decode(Tag), unicode_decode(Value)} | Acc]); _ -> {Body, Rest} = join_entry([Line | Lines], []), {lists:reverse(Acc), rfc4716_pubkey_decode(base64:mime_decode(Body)), Rest} end. join_entry([<<"---- END SSH2 PUBLIC KEY ----", _/binary>>| Lines], Entry) -> {lists:reverse(Entry), Lines}; join_entry([Line | Lines], Entry) -> join_entry(Lines, [Line | Entry]). rfc4716_pubkey_decode(<<?UINT32(Len), Type:Len/binary, ?UINT32(SizeE), E:SizeE/binary, ?UINT32(SizeN), N:SizeN/binary>>) when Type == <<"ssh-rsa">> -> #'RSAPublicKey'{modulus = erlint(SizeN, N), publicExponent = erlint(SizeE, E)}; rfc4716_pubkey_decode(<<?UINT32(Len), Type:Len/binary, ?UINT32(SizeP), P:SizeP/binary, ?UINT32(SizeQ), Q:SizeQ/binary, ?UINT32(SizeG), G:SizeG/binary, ?UINT32(SizeY), Y:SizeY/binary>>) when Type == <<"ssh-dss">> -> {erlint(SizeY, Y), #'Dss-Parms'{p = erlint(SizeP, P), q = erlint(SizeQ, Q), g = erlint(SizeG, G)}}. openssh_decode(Bin, FileType) -> Lines = binary:split(Bin, <<"\n">>, [global]), do_openssh_decode(FileType, Lines, []). do_openssh_decode(_, [], Acc) -> lists:reverse(Acc); %% Ignore empty lines do_openssh_decode(FileType, [<<>> | Lines], Acc) -> do_openssh_decode(FileType, Lines, Acc); %% Ignore lines that start with # do_openssh_decode(FileType,[<<"#", _/binary>> | Lines], Acc) -> do_openssh_decode(FileType, Lines, Acc); do_openssh_decode(auth_keys = FileType, [Line | Lines], Acc) -> Split = binary:split(Line, <<" ">>, [global]), case mend_split(Split, []) of %% ssh2 [KeyType, Base64Enc, Comment] -> do_openssh_decode(FileType, Lines, [{openssh_pubkey_decode(KeyType, Base64Enc), [{comment, string_decode(Comment)}]} | Acc]); %% ssh1 [Options, Bits, Exponent, Modulus, Comment] -> do_openssh_decode(FileType, Lines, [{ssh1_rsa_pubkey_decode(Modulus, Exponent), [{comment, string_decode(Comment)}, {options, comma_list_decode(Options)}, {bits, integer_decode(Bits)}]} | Acc]); [A, B, C, D] -> ssh_2_or_1(FileType, Lines, Acc, A,B,C,D) end; do_openssh_decode(known_hosts = FileType, [Line | Lines], Acc) -> Split = binary:split(Line, <<" ">>, [global]), case mend_split(Split, []) of %% ssh 2 [HostNames, KeyType, Base64Enc] -> do_openssh_decode(FileType, Lines, [{openssh_pubkey_decode(KeyType, Base64Enc), [{hostnames, comma_list_decode(HostNames)}]}| Acc]); [A, B, C, D] -> ssh_2_or_1(FileType, Lines, Acc, A, B, C, D); %% ssh 1 [HostNames, Bits, Exponent, Modulus, Comment] -> do_openssh_decode(FileType, Lines, [{ssh1_rsa_pubkey_decode(Modulus, Exponent), [{comment, string_decode(Comment)}, {hostnames, comma_list_decode(HostNames)}, {bits, integer_decode(Bits)}]} | Acc]) end; do_openssh_decode(openssh_public_key = FileType, [Line | Lines], Acc) -> Split = binary:split(Line, <<" ">>, [global]), case mend_split(Split, []) of [KeyType, Base64Enc, Comment0] when KeyType == <<"ssh-rsa">>; KeyType == <<"ssh-dss">> -> Comment = string:strip(binary_to_list(Comment0), right, $\n), do_openssh_decode(FileType, Lines, [{openssh_pubkey_decode(KeyType, Base64Enc), [{comment, Comment}]} | Acc]) end. ssh_2_or_1(known_hosts = FileType, Lines, Acc, A, B, C, D) -> try integer_decode(B) of Int -> file_type_decode_ssh1(FileType, Lines, Acc, A, Int, C,D) catch error:badarg -> file_type_decode_ssh2(FileType, Lines, Acc, A,B,C,D) end; ssh_2_or_1(auth_keys = FileType, Lines, Acc, A, B, C, D) -> try integer_decode(A) of Int -> file_type_decode_ssh1(FileType, Lines, Acc, Int, B, C,D) catch error:badarg -> file_type_decode_ssh2(FileType, Lines, Acc, A,B,C,D) end. file_type_decode_ssh1(known_hosts = FileType, Lines, Acc, HostNames, Bits, Exponent, Modulus) -> do_openssh_decode(FileType, Lines, [{ssh1_rsa_pubkey_decode(Modulus, Exponent), [{comment, []}, {hostnames, comma_list_decode(HostNames)}, {bits, Bits}]} | Acc]); file_type_decode_ssh1(auth_keys = FileType, Lines, Acc, Bits, Exponent, Modulus, Comment) -> do_openssh_decode(FileType, Lines, [{ssh1_rsa_pubkey_decode(Modulus, Exponent), [{comment, string_decode(Comment)}, {bits, Bits}]} | Acc]). file_type_decode_ssh2(known_hosts = FileType, Lines, Acc, HostNames, KeyType, Base64Enc, Comment) -> do_openssh_decode(FileType, Lines, [{openssh_pubkey_decode(KeyType, Base64Enc), [{comment, string_decode(Comment)}, {hostnames, comma_list_decode(HostNames)}]} | Acc]); file_type_decode_ssh2(auth_keys = FileType, Lines, Acc, Options, KeyType, Base64Enc, Comment) -> do_openssh_decode(FileType, Lines, [{openssh_pubkey_decode(KeyType, Base64Enc), [{comment, string_decode(Comment)}, {options, comma_list_decode(Options)}]} | Acc]). openssh_pubkey_decode(<<"ssh-rsa">>, Base64Enc) -> <<?UINT32(StrLen), _:StrLen/binary, ?UINT32(SizeE), E:SizeE/binary, ?UINT32(SizeN), N:SizeN/binary>> = base64:mime_decode(Base64Enc), #'RSAPublicKey'{modulus = erlint(SizeN, N), publicExponent = erlint(SizeE, E)}; openssh_pubkey_decode(<<"ssh-dss">>, Base64Enc) -> <<?UINT32(StrLen), _:StrLen/binary, ?UINT32(SizeP), P:SizeP/binary, ?UINT32(SizeQ), Q:SizeQ/binary, ?UINT32(SizeG), G:SizeG/binary, ?UINT32(SizeY), Y:SizeY/binary>> = base64:mime_decode(Base64Enc), {erlint(SizeY, Y), #'Dss-Parms'{p = erlint(SizeP, P), q = erlint(SizeQ, Q), g = erlint(SizeG, G)}}; openssh_pubkey_decode(KeyType, Base64Enc) -> {KeyType, base64:mime_decode(Base64Enc)}. erlint(MPIntSize, MPIntValue) -> Bits= MPIntSize * 8, <<Integer:Bits/integer>> = MPIntValue, Integer. ssh1_rsa_pubkey_decode(MBin, EBin) -> #'RSAPublicKey'{modulus = integer_decode(MBin), publicExponent = integer_decode(EBin)}. integer_decode(BinStr) -> list_to_integer(binary_to_list(BinStr)). string_decode(BinStr) -> binary_to_list(BinStr). unicode_decode(BinStr) -> unicode:characters_to_list(BinStr). comma_list_decode(BinOpts) -> CommaList = binary:split(BinOpts, <<",">>, [global]), lists:map(fun(Item) -> binary_to_list(Item) end, CommaList). do_encode(rfc4716_public_key, Key, Attributes) -> rfc4716_encode(Key, proplists:get_value(headers, Attributes, []), []); do_encode(Type, Key, Attributes) -> openssh_encode(Type, Key, Attributes). rfc4716_encode(Key, [],[]) -> erlang:iolist_to_binary([begin_marker(),"\n", split_lines(base64:encode(ssh2_pubkey_encode(Key))), "\n", end_marker(), "\n"]); rfc4716_encode(Key, [], [_|_] = Acc) -> erlang:iolist_to_binary([begin_marker(), "\n", lists:reverse(Acc), split_lines(base64:encode(ssh2_pubkey_encode(Key))), "\n", end_marker(), "\n"]); rfc4716_encode(Key, [ Header | Headers], Acc) -> LinesStr = rfc4716_encode_header(Header), rfc4716_encode(Key, Headers, [LinesStr | Acc]). rfc4716_encode_header({Tag, Value}) -> TagLen = length(Tag), ValueLen = length(Value), case TagLen + 1 + ValueLen of N when N > ?ENCODED_LINE_LENGTH -> NumOfChars = ?ENCODED_LINE_LENGTH - (TagLen + 1), {First, Rest} = lists:split(NumOfChars, Value), [Tag,":" , First, [$\\], "\n", rfc4716_encode_value(Rest) , "\n"]; _ -> [Tag, ":", Value, "\n"] end. rfc4716_encode_value(Value) -> case length(Value) of N when N > ?ENCODED_LINE_LENGTH -> {First, Rest} = lists:split(?ENCODED_LINE_LENGTH, Value), [First, [$\\], "\n", rfc4716_encode_value(Rest)]; _ -> Value end. openssh_encode(openssh_public_key, Key, Attributes) -> Comment = proplists:get_value(comment, Attributes), Enc = base64:encode(ssh2_pubkey_encode(Key)), erlang:iolist_to_binary([key_type(Key), " ", Enc, " ", Comment, "\n"]); openssh_encode(auth_keys, Key, Attributes) -> Comment = proplists:get_value(comment, Attributes, ""), Options = proplists:get_value(options, Attributes, undefined), Bits = proplists:get_value(bits, Attributes, undefined), case Bits of undefined -> openssh_ssh2_auth_keys_encode(Options, Key, Comment); _ -> openssh_ssh1_auth_keys_encode(Options, Bits, Key, Comment) end; openssh_encode(known_hosts, Key, Attributes) -> Comment = proplists:get_value(comment, Attributes, ""), Hostnames = proplists:get_value(hostnames, Attributes), Bits = proplists:get_value(bits, Attributes, undefined), case Bits of undefined -> openssh_ssh2_know_hosts_encode(Hostnames, Key, Comment); _ -> openssh_ssh1_known_hosts_encode(Hostnames, Bits, Key, Comment) end. openssh_ssh2_auth_keys_encode(undefined, Key, Comment) -> erlang:iolist_to_binary([key_type(Key)," ", base64:encode(ssh2_pubkey_encode(Key)), line_end(Comment)]); openssh_ssh2_auth_keys_encode(Options, Key, Comment) -> erlang:iolist_to_binary([comma_list_encode(Options, []), " ", key_type(Key)," ", base64:encode(ssh2_pubkey_encode(Key)), line_end(Comment)]). openssh_ssh1_auth_keys_encode(undefined, Bits, #'RSAPublicKey'{modulus = N, publicExponent = E}, Comment) -> erlang:iolist_to_binary([integer_to_list(Bits), " ", integer_to_list(E), " ", integer_to_list(N), line_end(Comment)]); openssh_ssh1_auth_keys_encode(Options, Bits, #'RSAPublicKey'{modulus = N, publicExponent = E}, Comment) -> erlang:iolist_to_binary([comma_list_encode(Options, []), " ", integer_to_list(Bits), " ", integer_to_list(E), " ", integer_to_list(N), line_end(Comment)]). openssh_ssh2_know_hosts_encode(Hostnames, Key, Comment) -> erlang:iolist_to_binary([comma_list_encode(Hostnames, []), " ", key_type(Key)," ", base64:encode(ssh2_pubkey_encode(Key)), line_end(Comment)]). openssh_ssh1_known_hosts_encode(Hostnames, Bits, #'RSAPublicKey'{modulus = N, publicExponent = E}, Comment) -> erlang:iolist_to_binary([comma_list_encode(Hostnames, [])," ", integer_to_list(Bits)," ", integer_to_list(E)," ", integer_to_list(N), line_end(Comment)]). line_end("") -> "\n"; line_end(Comment) -> [" ", Comment, "\n"]. key_type(#'RSAPublicKey'{}) -> <<"ssh-rsa">>; key_type({_, #'Dss-Parms'{}}) -> <<"ssh-dss">>. comma_list_encode([Option], []) -> Option; comma_list_encode([Option], Acc) -> Acc ++ "," ++ Option; comma_list_encode([Option | Rest], []) -> comma_list_encode(Rest, Option); comma_list_encode([Option | Rest], Acc) -> comma_list_encode(Rest, Acc ++ "," ++ Option). ssh2_pubkey_encode(#'RSAPublicKey'{modulus = N, publicExponent = E}) -> TypeStr = <<"ssh-rsa">>, StrLen = size(TypeStr), EBin = crypto:mpint(E), NBin = crypto:mpint(N), <<?UINT32(StrLen), TypeStr:StrLen/binary, EBin/binary, NBin/binary>>; ssh2_pubkey_encode({Y, #'Dss-Parms'{p = P, q = Q, g = G}}) -> TypeStr = <<"ssh-dss">>, StrLen = size(TypeStr), PBin = crypto:mpint(P), QBin = crypto:mpint(Q), GBin = crypto:mpint(G), YBin = crypto:mpint(Y), <<?UINT32(StrLen), TypeStr:StrLen/binary, PBin/binary, QBin/binary, GBin/binary, YBin/binary>>. mend_split([Part1, Part2 | Rest] = List, Acc) -> case option_end(Part1, Part2) of true -> lists:reverse(Acc) ++ List; false -> case length(binary:matches(Part1, <<"\"">>)) of N when N rem 2 == 0 -> mend_split(Rest, [Part1 | Acc]); _ -> mend_split([<<Part1/binary, Part2/binary>> | Rest], Acc) end end. option_end(Part1, Part2) -> (is_key_field(Part1) orelse is_bits_field(Part1)) orelse (is_key_field(Part2) orelse is_bits_field(Part2)). is_key_field(<<"ssh-dss">>) -> true; is_key_field(<<"ssh-rsa">>) -> true; is_key_field(<<"ecdsa-sha2-nistp256">>) -> true; is_key_field(<<"ecdsa-sha2-nistp384">>) -> true; is_key_field(<<"ecdsa-sha2-nistp521">>) -> true; is_key_field(_) -> false. is_bits_field(Part) -> try list_to_integer(binary_to_list(Part)) of _ -> true catch _:_ -> false end. split_lines(<<Text:?ENCODED_LINE_LENGTH/binary>>) -> [Text]; split_lines(<<Text:?ENCODED_LINE_LENGTH/binary, Rest/binary>>) -> [Text, $\n | split_lines(Rest)]; split_lines(Bin) -> [Bin].