diff options
Diffstat (limited to 'src/cyrsasl_digest.erl')
-rw-r--r-- | src/cyrsasl_digest.erl | 298 |
1 files changed, 155 insertions, 143 deletions
diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl index 557e498cd..3bb88431b 100644 --- a/src/cyrsasl_digest.erl +++ b/src/cyrsasl_digest.erl @@ -25,134 +25,145 @@ %%%---------------------------------------------------------------------- -module(cyrsasl_digest). + -author('alexey@sevcom.net'). --export([start/1, - stop/0, - mech_new/4, - mech_step/2]). +-export([start/1, stop/0, mech_new/4, mech_step/2, parse/1]). -include("ejabberd.hrl"). -behaviour(cyrsasl). --record(state, {step, nonce, username, authzid, get_password, check_password, auth_module, - host, hostfqdn}). +-type get_password_fun() :: fun((binary()) -> {false, any()} | + {binary(), atom()}). + +-type check_password_fun() :: fun((binary(), binary(), binary(), + fun((binary()) -> binary())) -> + {boolean(), any()} | + false). + +-record(state, {step = 1 :: 1 | 3 | 5, + nonce = <<"">> :: binary(), + username = <<"">> :: binary(), + authzid = <<"">> :: binary(), + get_password = fun(_) -> {false, <<>>} end :: get_password_fun(), + check_password = fun(_, _, _, _) -> false end :: check_password_fun(), + auth_module :: atom(), + host = <<"">> :: binary(), + hostfqdn = <<"">> :: binary()}). start(_Opts) -> Fqdn = get_local_fqdn(), - ?INFO_MSG("FQDN used to check DIGEST-MD5 SASL authentication: ~p", [Fqdn]), - cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, digest). + ?INFO_MSG("FQDN used to check DIGEST-MD5 SASL authentication: ~s", + [Fqdn]), + cyrsasl:register_mechanism(<<"DIGEST-MD5">>, ?MODULE, + digest). -stop() -> - ok. +stop() -> ok. -mech_new(Host, GetPassword, _CheckPassword, CheckPasswordDigest) -> - {ok, #state{step = 1, - nonce = randoms:get_string(), - host = Host, - hostfqdn = get_local_fqdn(), - get_password = GetPassword, - check_password = CheckPasswordDigest}}. +mech_new(Host, GetPassword, _CheckPassword, + CheckPasswordDigest) -> + {ok, + #state{step = 1, nonce = randoms:get_string(), + host = Host, hostfqdn = get_local_fqdn(), + get_password = GetPassword, + check_password = CheckPasswordDigest}}. mech_step(#state{step = 1, nonce = Nonce} = State, _) -> {continue, - "nonce=\"" ++ Nonce ++ - "\",qop=\"auth\",charset=utf-8,algorithm=md5-sess", + <<"nonce=\"", Nonce/binary, + "\",qop=\"auth\",charset=utf-8,algorithm=md5-sess">>, State#state{step = 3}}; -mech_step(#state{step = 3, nonce = Nonce} = State, ClientIn) -> +mech_step(#state{step = 3, nonce = Nonce} = State, + ClientIn) -> case parse(ClientIn) of - bad -> - {error, "bad-protocol"}; - KeyVals -> - DigestURI = xml:get_attr_s("digest-uri", KeyVals), - UserName = xml:get_attr_s("username", KeyVals), - case is_digesturi_valid(DigestURI, State#state.host, State#state.hostfqdn) of - false -> - ?DEBUG("User login not authorized because digest-uri " - "seems invalid: ~p (checking for Host ~p, FQDN ~p)", [DigestURI, - State#state.host, State#state.hostfqdn]), - {error, "not-authorized", UserName}; - true -> - AuthzId = xml:get_attr_s("authzid", KeyVals), - case (State#state.get_password)(UserName) of - {false, _} -> - {error, "not-authorized", UserName}; - {Passwd, AuthModule} -> - case (State#state.check_password)(UserName, "", - xml:get_attr_s("response", KeyVals), - fun(PW) -> response(KeyVals, UserName, PW, Nonce, AuthzId, - "AUTHENTICATE") end) of - {true, _} -> - RspAuth = response(KeyVals, - UserName, Passwd, - Nonce, AuthzId, ""), - {continue, - "rspauth=" ++ RspAuth, - State#state{step = 5, - auth_module = AuthModule, - username = UserName, - authzid = AuthzId}}; - false -> - {error, "not-authorized", UserName}; - {false, _} -> - {error, "not-authorized", UserName} - end - end - end + bad -> {error, <<"bad-protocol">>}; + KeyVals -> + DigestURI = proplists:get_value(<<"digest-uri">>, KeyVals, <<>>), + %DigestURI = xml:get_attr_s(<<"digest-uri">>, KeyVals), + UserName = proplists:get_value(<<"username">>, KeyVals, <<>>), + %UserName = xml:get_attr_s(<<"username">>, KeyVals), + case is_digesturi_valid(DigestURI, State#state.host, + State#state.hostfqdn) + of + false -> + ?DEBUG("User login not authorized because digest-uri " + "seems invalid: ~p (checking for Host " + "~p, FQDN ~p)", + [DigestURI, State#state.host, State#state.hostfqdn]), + {error, <<"not-authorized">>, UserName}; + true -> + AuthzId = proplists:get_value(<<"authzid">>, KeyVals, <<>>), + %AuthzId = xml:get_attr_s(<<"authzid">>, KeyVals), + case (State#state.get_password)(UserName) of + {false, _} -> {error, <<"not-authorized">>, UserName}; + {Passwd, AuthModule} -> + case (State#state.check_password)(UserName, <<"">>, + proplists:get_value(<<"response">>, KeyVals, <<>>), + %xml:get_attr_s(<<"response">>, KeyVals), + fun (PW) -> + response(KeyVals, + UserName, + PW, + Nonce, + AuthzId, + <<"AUTHENTICATE">>) + end) + of + {true, _} -> + RspAuth = response(KeyVals, UserName, Passwd, Nonce, + AuthzId, <<"">>), + {continue, <<"rspauth=", RspAuth/binary>>, + State#state{step = 5, auth_module = AuthModule, + username = UserName, + authzid = AuthzId}}; + false -> {error, <<"not-authorized">>, UserName}; + {false, _} -> {error, <<"not-authorized">>, UserName} + end + end + end end; -mech_step(#state{step = 5, - auth_module = AuthModule, - username = UserName, - authzid = AuthzId}, "") -> - {ok, [{username, UserName}, {authzid, AuthzId}, - {auth_module, AuthModule}]}; +mech_step(#state{step = 5, auth_module = AuthModule, + username = UserName, authzid = AuthzId}, + <<"">>) -> + {ok, + [{username, UserName}, {authzid, AuthzId}, + {auth_module, AuthModule}]}; mech_step(A, B) -> - ?DEBUG("SASL DIGEST: A ~p B ~p", [A,B]), - {error, "bad-protocol"}. + ?DEBUG("SASL DIGEST: A ~p B ~p", [A, B]), + {error, <<"bad-protocol">>}. -parse(S) -> - parse1(S, "", []). +parse(S) -> parse1(binary_to_list(S), "", []). parse1([$= | Cs], S, Ts) -> parse2(Cs, lists:reverse(S), "", Ts); -parse1([$, | Cs], [], Ts) -> - parse1(Cs, [], Ts); -parse1([$\s | Cs], [], Ts) -> - parse1(Cs, [], Ts); -parse1([C | Cs], S, Ts) -> - parse1(Cs, [C | S], Ts); -parse1([], [], T) -> - lists:reverse(T); -parse1([], _S, _T) -> - bad. - -parse2([$\" | Cs], Key, Val, Ts) -> +parse1([$, | Cs], [], Ts) -> parse1(Cs, [], Ts); +parse1([$\s | Cs], [], Ts) -> parse1(Cs, [], Ts); +parse1([C | Cs], S, Ts) -> parse1(Cs, [C | S], Ts); +parse1([], [], T) -> lists:reverse(T); +parse1([], _S, _T) -> bad. + +parse2([$" | Cs], Key, Val, Ts) -> parse3(Cs, Key, Val, Ts); parse2([C | Cs], Key, Val, Ts) -> parse4(Cs, Key, [C | Val], Ts); -parse2([], _, _, _) -> - bad. +parse2([], _, _, _) -> bad. -parse3([$\" | Cs], Key, Val, Ts) -> +parse3([$" | Cs], Key, Val, Ts) -> parse4(Cs, Key, Val, Ts); parse3([$\\, C | Cs], Key, Val, Ts) -> parse3(Cs, Key, [C | Val], Ts); parse3([C | Cs], Key, Val, Ts) -> parse3(Cs, Key, [C | Val], Ts); -parse3([], _, _, _) -> - bad. +parse3([], _, _, _) -> bad. parse4([$, | Cs], Key, Val, Ts) -> - parse1(Cs, "", [{Key, lists:reverse(Val)} | Ts]); + parse1(Cs, "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]); parse4([$\s | Cs], Key, Val, Ts) -> parse4(Cs, Key, Val, Ts); parse4([C | Cs], Key, Val, Ts) -> parse4(Cs, Key, [C | Val], Ts); parse4([], Key, Val, Ts) -> - parse1([], "", [{Key, lists:reverse(Val)} | Ts]). - - %% @doc Check if the digest-uri is valid. %% RFC-2831 allows to provide the IP address in Host, %% however ejabberd doesn't allow that. @@ -162,14 +173,17 @@ parse4([], Key, Val, Ts) -> %% xmpp/server3.example.org/jabber.example.org, xmpp/server3.example.org and %% xmpp/jabber.example.org %% The last version is not actually allowed by the RFC, but implemented by popular clients -is_digesturi_valid(DigestURICase, JabberDomain, JabberFQDN) -> + parse1([], "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]). + +is_digesturi_valid(DigestURICase, JabberDomain, + JabberFQDN) -> DigestURI = stringprep:tolower(DigestURICase), - case catch string:tokens(DigestURI, "/") of - ["xmpp", Host] -> - IsHostFqdn = is_host_fqdn(Host, JabberFQDN), + case catch str:tokens(DigestURI, <<"/">>) of + [<<"xmpp">>, Host] -> + IsHostFqdn = is_host_fqdn(binary_to_list(Host), binary_to_list(JabberFQDN)), (Host == JabberDomain) or IsHostFqdn; - ["xmpp", Host, ServName] -> - IsHostFqdn = is_host_fqdn(Host, JabberFQDN), + [<<"xmpp">>, Host, ServName] -> + IsHostFqdn = is_host_fqdn(binary_to_list(Host), binary_to_list(JabberFQDN)), (ServName == JabberDomain) and IsHostFqdn; _ -> false @@ -185,62 +199,60 @@ is_host_fqdn(Host, [Fqdn | FqdnTail]) when Host /= Fqdn -> is_host_fqdn(Host, FqdnTail). get_local_fqdn() -> - case (catch get_local_fqdn2()) of - Str when is_list(Str) -> Str; - _ -> "unknown-fqdn, please configure fqdn option in ejabberd.cfg!" + case catch get_local_fqdn2() of + Str when is_binary(Str) -> Str; + _ -> + <<"unknown-fqdn, please configure fqdn " + "option in ejabberd.cfg!">> end. + get_local_fqdn2() -> - case ejabberd_config:get_local_option(fqdn) of - ConfiguredFqdn when is_list(ConfiguredFqdn) -> - ConfiguredFqdn; - _undefined -> - {ok, Hostname} = inet:gethostname(), - {ok, {hostent, Fqdn, _, _, _, _}} = inet:gethostbyname(Hostname), - Fqdn + case ejabberd_config:get_local_option( + fqdn, fun iolist_to_binary/1) of + ConfiguredFqdn when is_binary(ConfiguredFqdn) -> + ConfiguredFqdn; + undefined -> + {ok, Hostname} = inet:gethostname(), + {ok, {hostent, Fqdn, _, _, _, _}} = + inet:gethostbyname(Hostname), + list_to_binary(Fqdn) end. -digit_to_xchar(D) when (D >= 0) and (D < 10) -> - D + 48; -digit_to_xchar(D) -> - D + 87. - hex(S) -> - hex(S, []). - -hex([], Res) -> - lists:reverse(Res); -hex([N | Ns], Res) -> - hex(Ns, [digit_to_xchar(N rem 16), - digit_to_xchar(N div 16) | Res]). + sha:to_hexlist(S). +proplists_get_bin_value(Key, Pairs, Default) -> + case proplists:get_value(Key, Pairs, Default) of + L when is_list(L) -> + list_to_binary(L); + L2 -> + L2 + end. -response(KeyVals, User, Passwd, Nonce, AuthzId, A2Prefix) -> - Realm = xml:get_attr_s("realm", KeyVals), - CNonce = xml:get_attr_s("cnonce", KeyVals), - DigestURI = xml:get_attr_s("digest-uri", KeyVals), - NC = xml:get_attr_s("nc", KeyVals), - QOP = xml:get_attr_s("qop", KeyVals), +response(KeyVals, User, Passwd, Nonce, AuthzId, + A2Prefix) -> + Realm = proplists_get_bin_value(<<"realm">>, KeyVals, <<>>), + CNonce = proplists_get_bin_value(<<"cnonce">>, KeyVals, <<>>), + DigestURI = proplists_get_bin_value(<<"digest-uri">>, KeyVals, <<>>), + NC = proplists_get_bin_value(<<"nc">>, KeyVals, <<>>), + QOP = proplists_get_bin_value(<<"qop">>, KeyVals, <<>>), + MD5Hash = crypto:md5(<<User/binary, ":", Realm/binary, ":", + Passwd/binary>>), A1 = case AuthzId of - "" -> - binary_to_list( - crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++ - ":" ++ Nonce ++ ":" ++ CNonce; - _ -> - binary_to_list( - crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++ - ":" ++ Nonce ++ ":" ++ CNonce ++ ":" ++ AuthzId + <<"">> -> + <<MD5Hash/binary, ":", Nonce/binary, ":", CNonce/binary>>; + _ -> + <<MD5Hash/binary, ":", Nonce/binary, ":", CNonce/binary, ":", + AuthzId/binary>> end, A2 = case QOP of - "auth" -> - A2Prefix ++ ":" ++ DigestURI; - _ -> - A2Prefix ++ ":" ++ DigestURI ++ - ":00000000000000000000000000000000" + <<"auth">> -> + <<A2Prefix/binary, ":", DigestURI/binary>>; + _ -> + <<A2Prefix/binary, ":", DigestURI/binary, + ":00000000000000000000000000000000">> end, - T = hex(binary_to_list(crypto:md5(A1))) ++ ":" ++ Nonce ++ ":" ++ - NC ++ ":" ++ CNonce ++ ":" ++ QOP ++ ":" ++ - hex(binary_to_list(crypto:md5(A2))), - hex(binary_to_list(crypto:md5(T))). - - - + T = <<(hex((crypto:md5(A1))))/binary, ":", Nonce/binary, + ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, + ":", (hex((crypto:md5(A2))))/binary>>, + hex((crypto:md5(T))). |