aboutsummaryrefslogtreecommitdiff
path: root/src/cyrsasl_scram.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cyrsasl_scram.erl')
-rw-r--r--src/cyrsasl_scram.erl197
1 files changed, 197 insertions, 0 deletions
diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl
new file mode 100644
index 000000000..42f33d7e2
--- /dev/null
+++ b/src/cyrsasl_scram.erl
@@ -0,0 +1,197 @@
+%%%----------------------------------------------------------------------
+%%% File : cyrsasl_scram.erl
+%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
+%%% Purpose : SASL SCRAM authentication
+%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2011 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module(cyrsasl_scram).
+-author('stephen.roettger@googlemail.com').
+
+-export([start/1,
+ stop/0,
+ mech_new/1,
+ mech_step/2]).
+
+-include("ejabberd.hrl").
+-include("cyrsasl.hrl").
+
+-behaviour(cyrsasl).
+
+-record(state, {step, stored_key, server_key, username, get_password, check_password,
+ auth_message, client_nonce, server_nonce}).
+
+-define(SALT_LENGTH, 16).
+-define(NONCE_LENGTH, 16).
+
+start(_Opts) ->
+ cyrsasl:register_mechanism("SCRAM-SHA-1", ?MODULE, scram).
+
+stop() ->
+ ok.
+
+mech_new(#sasl_params{get_password=GetPassword}) ->
+ {ok, #state{step = 2, get_password = GetPassword}}.
+
+mech_step(#state{step = 2} = State, ClientIn) ->
+ case string:tokens(ClientIn, ",") of
+ [CBind, UserNameAttribute, ClientNonceAttribute] when (CBind == "y") or (CBind == "n") ->
+ case parse_attribute(UserNameAttribute) of
+ {error, Reason} ->
+ {error, Reason};
+ {_, EscapedUserName} ->
+ case unescape_username(EscapedUserName) of
+ error ->
+ {error, 'protocol-error-bad-username'};
+ UserName ->
+ case parse_attribute(ClientNonceAttribute) of
+ {$r, ClientNonce} ->
+ case (State#state.get_password)(UserName) of
+ {false, _} ->
+ {error, 'not-authorized', "", UserName};
+ {Ret, _AuthModule} ->
+ {StoredKey, ServerKey, Salt, IterationCount} = if
+ is_tuple(Ret) ->
+ Ret;
+ true ->
+ TempSalt = crypto:rand_bytes(?SALT_LENGTH),
+ SaltedPassword = scram:salted_password(Ret, TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT),
+ {scram:stored_key(scram:client_key(SaltedPassword)),
+ scram:server_key(SaltedPassword), TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT}
+ end,
+ ClientFirstMessageBare = string:substr(ClientIn, string:str(ClientIn, "n=")),
+ ServerNonce = base64:encode_to_string(crypto:rand_bytes(?NONCE_LENGTH)),
+ ServerFirstMessage = "r=" ++ ClientNonce ++ ServerNonce ++ "," ++
+ "s=" ++ base64:encode_to_string(Salt) ++ "," ++
+ "i=" ++ integer_to_list(IterationCount),
+ {continue,
+ ServerFirstMessage,
+ State#state{step = 4, stored_key = StoredKey, server_key = ServerKey,
+ auth_message = ClientFirstMessageBare ++ "," ++ ServerFirstMessage,
+ client_nonce = ClientNonce, server_nonce = ServerNonce, username = UserName}}
+ end;
+ _Else ->
+ {error, 'not-supported'}
+ end
+ end;
+ _Else ->
+ {error, 'bad-protocol'}
+ end;
+ _Else ->
+ {error, 'bad-protocol'}
+ end;
+mech_step(#state{step = 4} = State, ClientIn) ->
+ case string:tokens(ClientIn, ",") of
+ [GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
+ case parse_attribute(GS2ChannelBindingAttribute) of
+ {$c, CVal} when (CVal == "biws") or (CVal == "eSws") ->
+ %% biws is base64 for n,, => channelbinding not supported
+ %% eSws is base64 for y,, => channelbinding supported by client only
+ Nonce = State#state.client_nonce ++ State#state.server_nonce,
+ case parse_attribute(NonceAttribute) of
+ {$r, CompareNonce} when CompareNonce == Nonce ->
+ case parse_attribute(ClientProofAttribute) of
+ {$p, ClientProofB64} ->
+ ClientProof = base64:decode(ClientProofB64),
+ AuthMessage = State#state.auth_message ++ "," ++ string:substr(ClientIn, 1, string:str(ClientIn, ",p=")-1),
+ ClientSignature = scram:client_signature(State#state.stored_key, AuthMessage),
+ ClientKey = scram:client_key(ClientProof, ClientSignature),
+ CompareStoredKey = scram:stored_key(ClientKey),
+ if CompareStoredKey == State#state.stored_key ->
+ ServerSignature = scram:server_signature(State#state.server_key, AuthMessage),
+ {ok, [{username, State#state.username}], "v=" ++ base64:encode_to_string(ServerSignature)};
+ true ->
+ {error, 'bad-auth'}
+ end;
+ _Else ->
+ {error, 'bad-protocol'}
+ end;
+ {$r, _} ->
+ {error, 'bad-nonce'};
+ _Else ->
+ {error, 'bad-protocol'}
+ end;
+ _Else ->
+ {error, 'bad-protocol'}
+ end;
+ _Else ->
+ {error, 'bad-protocol'}
+ end.
+
+parse_attribute(Attribute) ->
+ AttributeLen = string:len(Attribute),
+ if
+ AttributeLen > 3 ->
+ SecondChar = lists:nth(2, Attribute),
+ case is_alpha(lists:nth(1, Attribute)) of
+ true ->
+ if
+ SecondChar == $= ->
+ case string:substr(Attribute, 3) of
+ String when is_list(String) ->
+ {lists:nth(1, Attribute), String};
+ _Else ->
+ {error, 'bad-format failed'}
+ end;
+ true ->
+ {error, 'bad-format second char not equal sign'}
+ end;
+ _Else ->
+ {error, 'bad-format first char not a letter'}
+ end;
+ true ->
+ {error, 'bad-format attribute too short'}
+ end.
+
+unescape_username("") ->
+ "";
+unescape_username(EscapedUsername) ->
+ Pos = string:str(EscapedUsername, "="),
+ if
+ Pos == 0 ->
+ EscapedUsername;
+ true ->
+ Start = string:substr(EscapedUsername, 1, Pos-1),
+ End = string:substr(EscapedUsername, Pos),
+ EndLen = string:len(End),
+ if
+ EndLen < 3 ->
+ error;
+ true ->
+ case string:substr(End, 1, 3) of
+ "=2C" ->
+ Start ++ "," ++ unescape_username(string:substr(End, 4));
+ "=3D" ->
+ Start ++ "=" ++ unescape_username(string:substr(End, 4));
+ _Else ->
+ error
+ end
+ end
+ end.
+
+is_alpha(Char) when Char >= $a, Char =< $z ->
+ true;
+is_alpha(Char) when Char >= $A, Char =< $Z ->
+ true;
+is_alpha(_) ->
+ true.
+