1 |
|
%% Copyright (c) 2022 Peter Morgan <peter.james.morgan@gmail.com> |
2 |
|
%% |
3 |
|
%% Licensed under the Apache License, Version 2.0 (the "License"); |
4 |
|
%% you may not use this file except in compliance with the License. |
5 |
|
%% You may obtain a copy of the License at |
6 |
|
%% |
7 |
|
%% http://www.apache.org/licenses/LICENSE-2.0 |
8 |
|
%% |
9 |
|
%% Unless required by applicable law or agreed to in writing, software |
10 |
|
%% distributed under the License is distributed on an "AS IS" BASIS, |
11 |
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 |
|
%% See the License for the specific language governing permissions and |
13 |
|
%% limitations under the License. |
14 |
|
|
15 |
|
|
16 |
|
-module(pgmp_mm_auth_sasl). |
17 |
|
|
18 |
|
|
19 |
|
-export([callback_mode/0]). |
20 |
|
-export([handle_event/4]). |
21 |
|
-import(pgmp_codec, [marshal/2]). |
22 |
|
-import(pgmp_codec, [size_inclusive/1]). |
23 |
|
-import(pgmp_statem, [nei/1]). |
24 |
|
-include_lib("kernel/include/logger.hrl"). |
25 |
|
|
26 |
|
|
27 |
|
callback_mode() -> |
28 |
66 |
[handle_event_function, state_enter]. |
29 |
|
|
30 |
|
|
31 |
|
handle_event(internal, {recv, {authentication, authenticated}}, _, Data) -> |
32 |
66 |
{next_state, authenticated, Data, pop_callback_module}; |
33 |
|
|
34 |
|
handle_event(internal, {recv, {error_response, Errors}}, _, Data) -> |
35 |
:-( |
{next_state, |
36 |
|
startup_failure, |
37 |
|
Data#{errors => Errors}, |
38 |
|
pop_callback_module}; |
39 |
|
|
40 |
|
%% |
41 |
|
%% https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism |
42 |
|
%% |
43 |
|
|
44 |
|
handle_event(internal, |
45 |
|
{sasl = EventName, [<<"SCRAM-SHA-256">> = Mechanism | _]}, |
46 |
|
_, |
47 |
|
Data) -> |
48 |
66 |
{keep_state, |
49 |
|
Data#{sasl => #{mechanism => Mechanism}}, |
50 |
|
[nei({telemetry, EventName, #{count => 1}, #{mechanism => Mechanism}}), |
51 |
|
|
52 |
|
nei({send, |
53 |
|
["p", |
54 |
|
size_inclusive( |
55 |
|
[marshal(string, Mechanism), marshal(int32, -1)])]})]}; |
56 |
|
|
57 |
|
handle_event(internal, |
58 |
|
{sasl = EventName, [<<"SCRAM-SHA-256-PLUS">> | Mechanisms]}, |
59 |
|
_, |
60 |
|
_) -> |
61 |
66 |
{keep_state_and_data, nei({EventName, Mechanisms})}; |
62 |
|
|
63 |
|
handle_event(internal, |
64 |
|
{recv = EventName, {authentication = Tag, {Action, Encoded}}}, |
65 |
|
_, |
66 |
|
#{sasl := #{mechanism := <<"SCRAM-SHA-256">>}}) |
67 |
|
when Action == sasl_continue; Action == sasl_final -> |
68 |
198 |
{keep_state_and_data, |
69 |
|
[nei({telemetry, |
70 |
|
EventName, |
71 |
|
#{count => 1}, |
72 |
|
#{tag => Tag, action => Action}}), |
73 |
|
|
74 |
|
nei({Action, Encoded, pgmp_scram:decode(Encoded)})]}; |
75 |
|
|
76 |
|
handle_event(internal, |
77 |
|
{sasl_final, _, #{v := V}}, |
78 |
|
_, |
79 |
|
#{sasl := #{client := #{v := V}}}) -> |
80 |
66 |
keep_state_and_data; |
81 |
|
|
82 |
|
handle_event( |
83 |
|
internal, |
84 |
|
{sasl_continue, |
85 |
|
ServerFirstMessage, |
86 |
|
#{r := R, s := Salt, i := I} = Server}, |
87 |
|
_, |
88 |
|
#{config := #{password := Password}, |
89 |
|
sasl := #{client := #{header := Header, nonce := Nonce} = Client, |
90 |
|
mechanism := <<"SCRAM-SHA-256">> = Mechanism} = SASL} = Data) -> |
91 |
|
|
92 |
|
%% SaltedPassword := Hi(Normalize(password), salt, i) |
93 |
66 |
SaltedPassword = pgmp_scram:salted_password( |
94 |
|
Mechanism, |
95 |
|
pgmp_scram:normalize(Password()), |
96 |
|
Salt, |
97 |
|
I), |
98 |
|
|
99 |
|
%% ClientKey := HMAC(SaltedPassword, "Client Key") |
100 |
66 |
ClientKey = pgmp_scram:client_key(Mechanism, SaltedPassword), |
101 |
|
|
102 |
|
%% StoredKey := H(ClientKey) |
103 |
66 |
StoredKey = pgmp_scram:stored_key(Mechanism, ClientKey), |
104 |
|
|
105 |
|
%% AuthMessage := client-first-message-bare + "," + |
106 |
|
%% server-first-message + "," + |
107 |
|
%% client-final-message-without-proof |
108 |
66 |
ClientFirstBare = pgmp_scram:client_first_bare(<<>>, Nonce), |
109 |
|
|
110 |
66 |
ClientFinalWithoutProof = pgmp_scram:client_final_without_proof( |
111 |
|
Header, |
112 |
|
R), |
113 |
|
|
114 |
66 |
AuthMessage = pgmp_scram:auth_message( |
115 |
|
ClientFirstBare, |
116 |
|
ServerFirstMessage, |
117 |
|
ClientFinalWithoutProof), |
118 |
|
|
119 |
|
%% ClientSignature := HMAC(StoredKey, AuthMessage) |
120 |
66 |
ClientSignature = pgmp_scram:client_signature( |
121 |
|
Mechanism, |
122 |
|
StoredKey, |
123 |
|
AuthMessage), |
124 |
|
|
125 |
|
%% ClientProof := ClientKey XOR ClientSignature |
126 |
66 |
ClientProof = pgmp_scram:client_proof( |
127 |
|
ClientKey, |
128 |
|
ClientSignature), |
129 |
|
|
130 |
|
%% ServerKey := HMAC(SaltedPassword, "Server Key") |
131 |
66 |
ServerKey = pgmp_scram:server_key(Mechanism, SaltedPassword), |
132 |
|
|
133 |
|
%% ServerSignature := HMAC(ServerKey, AuthMessage) |
134 |
66 |
ServerSignature = pgmp_scram:server_signature(Mechanism, ServerKey, AuthMessage), |
135 |
|
|
136 |
66 |
{keep_state, |
137 |
|
Data#{sasl := SASL#{server => Server, |
138 |
|
client := Client#{v => ServerSignature}}}, |
139 |
|
nei({send, |
140 |
|
["p", |
141 |
|
size_inclusive( |
142 |
|
[marshal( |
143 |
|
byte, |
144 |
|
pgmp_scram:client_final( |
145 |
|
Header, |
146 |
|
R, |
147 |
|
ClientProof))])]})}; |
148 |
|
|
149 |
|
handle_event(internal, |
150 |
|
{sasl_continue, <<>>, _}, |
151 |
|
_, |
152 |
|
#{sasl := #{mechanism := <<"SCRAM-SHA-256">>} = SASL} = Data) -> |
153 |
66 |
Header = "n,,", |
154 |
66 |
Nonce = base64:encode(crypto:strong_rand_bytes(21)), |
155 |
66 |
{keep_state, |
156 |
|
Data#{sasl := SASL#{client => #{header => Header, nonce => Nonce}}}, |
157 |
|
nei({send, |
158 |
|
["p", |
159 |
|
size_inclusive( |
160 |
|
[marshal( |
161 |
|
byte, |
162 |
|
[Header, |
163 |
|
pgmp_scram:client_first_bare( |
164 |
|
<<>>, |
165 |
|
Nonce)])])]})}; |
166 |
|
|
167 |
|
handle_event(EventType, EventContent, State, Data) -> |
168 |
2112 |
pgmp_mm_common:handle_event(EventType, |
169 |
|
EventContent, |
170 |
|
State, |
171 |
|
Data). |