1
%%%----------------------------------------------------------------------
2
%%% File : cyrsasl_scram.erl
3
%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
4
%%% Purpose : SASL SCRAM authentication
5
%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
8
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
%%% General Public License for more details.
20
%%% You should have received a copy of the GNU General Public License
21
%%% along with this program; if not, write to the Free Software
22
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
25
%%%----------------------------------------------------------------------
27
-module(cyrsasl_scram).
28
-author('stephen.roettger@googlemail.com').
35
-include("ejabberd.hrl").
39
-record(state, {step, stored_key, server_key, username, get_password, check_password,
40
auth_message, client_nonce, server_nonce}).
42
-define(SALT_LENGTH, 16).
43
-define(NONCE_LENGTH, 16).
46
cyrsasl:register_mechanism("SCRAM-SHA-1", ?MODULE, scram).
51
mech_new(_Host, GetPassword, _CheckPassword, _CheckPasswordDigest) ->
52
{ok, #state{step = 2, get_password = GetPassword}}.
54
mech_step(#state{step = 2} = State, ClientIn) ->
55
case string:tokens(ClientIn, ",") of
56
[CBind, UserNameAttribute, ClientNonceAttribute] when (CBind == "y") or (CBind == "n") ->
57
case parse_attribute(UserNameAttribute) of
60
{_, EscapedUserName} ->
61
case unescape_username(EscapedUserName) of
63
{error, "protocol-error-bad-username"};
65
case parse_attribute(ClientNonceAttribute) of
67
case (State#state.get_password)(UserName) of
69
{error, "not-authorized", UserName};
71
{StoredKey, ServerKey, Salt, IterationCount} = if
75
TempSalt = crypto:rand_bytes(?SALT_LENGTH),
76
SaltedPassword = scram:salted_password(Ret, TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT),
77
{scram:stored_key(scram:client_key(SaltedPassword)),
78
scram:server_key(SaltedPassword), TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT}
80
ClientFirstMessageBare = string:substr(ClientIn, string:str(ClientIn, "n=")),
81
ServerNonce = base64:encode_to_string(crypto:rand_bytes(?NONCE_LENGTH)),
82
ServerFirstMessage = "r=" ++ ClientNonce ++ ServerNonce ++ "," ++
83
"s=" ++ base64:encode_to_string(Salt) ++ "," ++
84
"i=" ++ integer_to_list(IterationCount),
87
State#state{step = 4, stored_key = StoredKey, server_key = ServerKey,
88
auth_message = ClientFirstMessageBare ++ "," ++ ServerFirstMessage,
89
client_nonce = ClientNonce, server_nonce = ServerNonce, username = UserName}}
92
{error, "not-supported"}
97
{error, "bad-protocol"}
99
mech_step(#state{step = 4} = State, ClientIn) ->
100
case string:tokens(ClientIn, ",") of
101
[GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
102
case parse_attribute(GS2ChannelBindingAttribute) of
103
{$c, CVal} when (CVal == "biws") or (CVal == "eSws") ->
104
%% biws is base64 for n,, => channelbinding not supported
105
%% eSws is base64 for y,, => channelbinding supported by client only
106
Nonce = State#state.client_nonce ++ State#state.server_nonce,
107
case parse_attribute(NonceAttribute) of
108
{$r, CompareNonce} when CompareNonce == Nonce ->
109
case parse_attribute(ClientProofAttribute) of
110
{$p, ClientProofB64} ->
111
ClientProof = base64:decode(ClientProofB64),
112
AuthMessage = State#state.auth_message ++ "," ++ string:substr(ClientIn, 1, string:str(ClientIn, ",p=")-1),
113
ClientSignature = scram:client_signature(State#state.stored_key, AuthMessage),
114
ClientKey = scram:client_key(ClientProof, ClientSignature),
115
CompareStoredKey = scram:stored_key(ClientKey),
116
if CompareStoredKey == State#state.stored_key ->
117
ServerSignature = scram:server_signature(State#state.server_key, AuthMessage),
118
{ok, [{username, State#state.username}], "v=" ++ base64:encode_to_string(ServerSignature)};
123
{error, "bad-protocol"}
126
{error, "bad-nonce"};
128
{error, "bad-protocol"}
131
{error, "bad-protocol"}
134
{error, "bad-protocol"}
137
parse_attribute(Attribute) ->
138
AttributeLen = string:len(Attribute),
141
SecondChar = lists:nth(2, Attribute),
142
case is_alpha(lists:nth(1, Attribute)) of
146
String = string:substr(Attribute, 3),
147
{lists:nth(1, Attribute), String};
149
{error, "bad-format second char not equal sign"}
152
{error, "bad-format first char not a letter"}
155
{error, "bad-format attribute too short"}
158
unescape_username("") ->
160
unescape_username(EscapedUsername) ->
161
Pos = string:str(EscapedUsername, "="),
166
Start = string:substr(EscapedUsername, 1, Pos-1),
167
End = string:substr(EscapedUsername, Pos),
168
EndLen = string:len(End),
173
case string:substr(End, 1, 3) of
175
Start ++ "," ++ unescape_username(string:substr(End, 4));
177
Start ++ "=" ++ unescape_username(string:substr(End, 4));
184
is_alpha(Char) when Char >= $a, Char =< $z ->
186
is_alpha(Char) when Char >= $A, Char =< $Z ->