├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── rebar.config ├── rebar.lock ├── src ├── fast_scram.app.src ├── fast_scram.erl ├── fast_scram.hrl ├── fast_scram_attributes.erl ├── fast_scram_configuration.erl ├── fast_scram_definitions.erl └── fast_scram_parse_rules.erl └── test └── scram_SUITE.erl /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | name: OTP ${{matrix.otp}} 13 | strategy: 14 | matrix: 15 | otp: ['28', '27', '26'] 16 | rebar3: ['3.25.0'] 17 | runs-on: 'ubuntu-24.04' 18 | env: 19 | OTPVER: ${{ matrix.otp }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: erlef/setup-beam@v1 23 | with: 24 | otp-version: ${{matrix.otp}} 25 | rebar3-version: ${{matrix.rebar3}} 26 | - run: rebar3 fmt --check 27 | - run: rebar3 lint 28 | - run: rebar3 do ct --cover 29 | - run: rebar3 as test codecov analyze 30 | - run: rebar3 dialyzer 31 | if: ${{ matrix.otp == '28' }} 32 | - name: Upload code coverage 33 | uses: codecov/codecov-action@v4 34 | if: ${{ matrix.otp == '28' }} 35 | with: 36 | files: _build/test/covertool/fast_scram.covertool.xml 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | fail_ci_if_error: true 39 | verbose: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.d 6 | *.beam 7 | *.plt 8 | *.swp 9 | *.swo 10 | .erlang.cookie 11 | ebin 12 | log 13 | erl_crash.dump 14 | .rebar 15 | logs 16 | _build 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | *~ 21 | *.vim 22 | tags 23 | priv/ 24 | doc/ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020, Nelson Vides . 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast SCRAM 2 | 3 | [![Actions Status](https://github.com/esl/fast_scram/workflows/ci/badge.svg)](https://github.com/esl/fast_scram/actions) 4 | [![codecov](https://codecov.io/gh/esl/fast_scram/branch/master/graph/badge.svg)](https://codecov.io/gh/esl/fast_scram) 5 | [![Hex](http://img.shields.io/hexpm/v/fast_scram.svg)](https://hex.pm/packages/fast_scram) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/fast_scram/) 7 | 8 | `fast_scram` is a purely-functional Erlang implementation of the _Salted Challenge Response Authentication Mechanism_, where the challenge algorithm is implemented as a carefully-optimised NIF using [fast_pbkdf2][fast_pbkdf2]. 9 | 10 | ## Building 11 | `fast_scram` is a rebar3-compatible OTP application, building is as easy as `rebar3 compile`, and using it in your projects as 12 | ```erlang 13 | {plugins, [pc]}. 14 | {provider_hooks, 15 | [{pre, 16 | [{compile, {pc, compile}}, 17 | {clean, {pc, clean}}]}]}. 18 | {deps, 19 | [{fast_scram, "0.6.0"}]}. 20 | ``` 21 | 22 | ## Using 23 | ### SCRAM 24 | In SCRAM, a `SaltedPassword` is defined as 25 | ``` 26 | SaltedPassword := Hi(Normalize(password), salt, i) 27 | ``` 28 | This algorithm is precisely the one that pays the challenge, and it is the one we solve here with the best performance. 29 | Simply do: 30 | ```erlang 31 | SaltedPassword = fast_scram:hi(Hash, Password, Salt, IterationCount) 32 | ``` 33 | where `Hash` is the underlying hash function chosen as described by 34 | ```erlang 35 | -type sha_type() :: crypto:sha1() | crypto:sha2() | crypto:sha3(). 36 | ``` 37 | 38 | ### Full algorithm 39 | If you want to avoid reimplementing SCRAM again and again, you can use the extended API. 40 | The best example is that one of the tests. Given already configured states, the flow is as follows: 41 | ```erlang 42 | %% AUTH 43 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 44 | %% CHALLENGE 45 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 46 | %% RESPONSE 47 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 48 | %% SUCCESS 49 | {ok, ServerFinal, ServerFinalState} = fast_scram:mech_step(ServerState4, ClientFinal), 50 | %% Client successfully accepts the server's verifier 51 | {ok, ClientFinal, ClientFinalState} = fast_scram:mech_step(ClientState5, ServerFinal). 52 | ``` 53 | 54 | The API is simple: `fast_scram:mech_step/2` takes a SCRAM state, and the last message it received 55 | (in the case of the first step of the client, this is obviously, and necessarily, empty). 56 | 57 | The return value is always a 3-tuple, tagged with either `ok`, `continue` or `error`. 58 | The second element is always a binary, and the third is always the scram state. 59 | ```erlang 60 | -spec mech_step(fast_scram_state(), binary()) -> 61 | {ok, final_message(), fast_scram_state()} | 62 | {continue, next_message(), fast_scram_state()} | 63 | {error, error_message(), fast_scram_state()}. 64 | ``` 65 | 66 | * `ok` tagged-tuples mean that the algorithm has returned successfully. 67 | The message will be the last one to send to the peer, 68 | empty in the case of the client, containing the server verifier for the server. 69 | The state will not be needed anymore, so it can be ignored. 70 | * `continue` means that the algorithm is not done yet. The message is what needs to be send to the 71 | peer, by whatever means the protocol chooses (encoded in a major packet through some network 72 | protocol, etc). The new state is the one that should be plugged into the next step, 73 | when the peer has answered. 74 | * `error` means that the algorithm is over, unsuccessfully, where the message contains some 75 | explanation. The state might include parsed data or be return as it was. 76 | 77 | How messages are delivered to peers is part of the protocol within which SCRAM is embedded: 78 | for example, in XMPP, messages are delivered as special stanzas with the SCRAM payload encoded in 79 | `base64`. So an XMPP client would do, for example, using [exml][exml] 80 | ```erlang 81 | {continue, Message, NewState} = fast_scram:mech_step(State, <<>>), 82 | Contents = #xmlcdata{content = base64:encode(Message)}, 83 | Stanza = #xmlel{name = <<"auth">>, 84 | attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}, 85 | {<<"mechanism">>, <<"SCRAM-SHA-1">>}], 86 | children = [Contents]}, 87 | %% send stanza 88 | ``` 89 | 90 | ### Configuration 91 | This is the part that requires some knowledge of the SCRAM protocol. 92 | A ready SCRAM state is built using `fast_scram:mech_new/1`, 93 | which takes a map with the configuration parameters. 94 | 95 | Example configurations are, for the client: 96 | ```erlang 97 | #{entity => client, 98 | hash_method => sha, 99 | username => <<"user">>, 100 | auth_data => #{password => <<"somesupersafepassword">>}} 101 | ``` 102 | 103 | And for a server: 104 | ```erlang 105 | #{entity => server, 106 | hash_method => sha, 107 | nonce => <<"3rfcNHYJY1ZVvWVs7j">>, 108 | retrieve_mechanism => fun(Username) -> MoreConfig end} 109 | ``` 110 | 111 | *NOTE*: SCRAM requires the username and password to be Normalized using the SASLprep profile of the 112 | stringprep algorithm. Stringprepping algorithms would introduce a dependency to this repository that 113 | I didn't want to, so it is left to the user of this plugin to provide stringprepped binaries from 114 | the get-go. 115 | 116 | The first and most important key is the `entity` key, 117 | which takes two values: `client` or `server`. 118 | The next necessary key is the negotiated `hash_method`, 119 | that is, which of the `SHA` algorithms will be executed. 120 | Can be any of the OTP's `crypto:sha1() | crypto:sha2()`. 121 | 122 | Next keys depend on the chosen entity. 123 | If you want to configure a `client` state, then a `username` key is required. 124 | If you want to configure a `server` state, then `retrieve_mechanism` is required. 125 | 126 | Next, for both cases, an `auth_data` key is required. The value for this key is a map containing the 127 | minimum necessary information for executing a SCRAM algorithm: often just a `password`. 128 | But often, to avoid the challenge penalty, servers and client cache certain keys, 129 | considering that a server often gives the same salt and iteration count for a specific client. 130 | So we can instead cache `salted_password`, or a pair `stored_key`-`server_key`, 131 | or a pair `client_key`-`server_key`. All these pairs can be given with a `password` as a fallback, 132 | if the algorithm was to need recalculation. 133 | 134 | If the client is being given any cached configuration, it will simply attempt that data regardless 135 | of the challenge that the server requests from him. If verification was desired instead of failing, 136 | the main config map can take keys `cached_it_count` and `cached_salt`, and these will be verified 137 | against the challenge requested by the server: if it matches, the cached data will be used. If it 138 | doesn't, all data will be recalculated using the `password` key in the `auth_data` map, provided it 139 | is available. 140 | 141 | Channel binding specification can also be given by `channel_binding => {Type, Data}`, 142 | where `Type` is the channel binding name, and `Data` is its associated payload. 143 | The default is `{undefined, <<>>}`, which will set the gs2 flag to no binding, that is, `<<"n">>`. 144 | If for example a client had channel binding, but saw the server not offering any, 145 | this client should set the flag to `{none, <<>>}`: this will send the gs2 flag as `<<"y">>`. 146 | 147 | ### Server retrieval of the client's data 148 | 149 | SCRAM requires that the server retrieves the user's data with the username as exactly given 150 | in the client's first message. To configure this, a `retrieve_mechanism` key is required, 151 | whose value is a function of the type: 152 | 153 | ```erlang 154 | -type retrieve_mechanism() :: fun((username()) -> configuration()) 155 | | fun((username(), fast_scram_state()) -> 156 | {configuration(), fast_scram_state()}). 157 | ``` 158 | 159 | That is, a function object that: 160 | * Takes a username and returns more configuration to append to the state 161 | * Takes a username and the current state, and returns a pair of the extended 162 | configuration and a possibly new state. 163 | 164 | See examples below. 165 | 166 | #### `fun((username()) -> configuration())` 167 | 168 | ```erlang 169 | Fun = fun(Username) -> 170 | %% Get scram data for this user from the database 171 | ... 172 | %%% {StoredKey, ServerKey, Salt, ItCount} -> 173 | ... 174 | #{salt => Salt, 175 | it_count => ItCount, 176 | auth_data => #{stored_key => StoredKey, 177 | server_key => ServerKey}} 178 | end, 179 | {ok, State} = fast_scram:mech_new( 180 | #{entity => server, hash_method => Sha, retrieve_mechanism => Fun}). 181 | ``` 182 | 183 | #### `fun((username(), fast_scram_state()) -> {configuration(), fast_scram_state()}).` 184 | 185 | ```erlang 186 | Fun = fun(Username, State0) -> 187 | %% Get scram data for this user from the database 188 | ... 189 | %%% {StoredKey, ServerKey, Salt, ItCount} -> 190 | ... 191 | Config = #{salt => Salt, 192 | it_count => ItCount, 193 | auth_data => #{ 194 | stored_key => StoredKey, 195 | server_key => ServerKey}} 196 | 197 | %% Custom data can also be stored in the state to be extracted later 198 | State1 = fast_scram:mech_set(some_key, SomeData, State0), 199 | {Config, State1} 200 | end, 201 | {ok, State} = fast_scram:mech_new( 202 | #{entity => server, hash_method => Sha, retrieve_mechanism => Fun}). 203 | ``` 204 | 205 | ## Performance 206 | 207 | ### The problem 208 | SCRAM is a challenge-response authentication method, that is, it forces the client to compute a challenge in order to authenticate him. 209 | But when the server implementation is slower than that of an attacker, it makes the server vulnerable to DoS by hogging itself with computations. 210 | We could see that on the CI and load-testing pipelines of [MongooseIM][MIM] for example. 211 | 212 | ### The solution 213 | Is partial. We don't expect to have the fastest implementation, as that would be purely C code on GPUs, so unfortunately an attacker will pretty much always have better chances there. 214 | _But_ we can make the computation cheap enough for us that other computations —like the load of a session establishment— will be more relevant than that of the challenge; 215 | and also that other defence mechanisms like IP blacklisting or traffic shaping, will fire in good time. 216 | 217 | ### The outcome 218 | It all boils down to the right PBKDF2 implementation, as done in [fast_pbkdf2][fast_pbkdf2], which is on average 10x faster on the machines I've tested it. 219 | But while the erlang implementation consumes memory linearly to the iteration count, the NIF implementation does not allocate any more memory. 220 | 221 | ### Running without NIFs 222 | Note that since OTP24.2, the `crypto` application exposes a native `pbkdf2_hmac` function. However, the NIF implementation from [fast_pbkdf2] is ~30% faster, and supports SHA3, so that one is left as a default. 223 | 224 | If for any particular reason you want to skip compiling and loading custom NIFs, you can override the dependency using `rebar3`'s overrides as below, this way the dependency will not be fetched and code will be compiled to use the `crypto` callback instead. 225 | 226 | ```erlang 227 | {overrides, 228 | [ 229 | {override, fast_scram, [{erl_opts, [{d, 'WITHOUT_NIFS'}]}, {deps, []}]}, 230 | ] 231 | }. 232 | ``` 233 | 234 | ## Read more: 235 | * SCRAM: [RFC5802](https://tools.ietf.org/html/rfc5802) 236 | * SCRAM-SHA-256 update: [RFC7677](https://tools.ietf.org/html/rfc7677) 237 | * Password-Based Cryptography Specification (PBKDF2): [RFC8018](https://tools.ietf.org/html/rfc8018) 238 | * HMAC: [RFC2104](https://tools.ietf.org/html/rfc2104) 239 | * SHAs and HMAC-SHA: [RFC6234](https://tools.ietf.org/html/rfc6234) 240 | 241 | [MIM]: https://github.com/esl/MongooseIM 242 | [exml]: https://github.com/esl/exml/ 243 | [fast_pbkdf2]: https://hex.pm/packages/fast_pbkdf2 244 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {fast_pbkdf2, "~> 2.0"} 5 | ]}. 6 | 7 | {project_plugins, [ 8 | {rebar3_hex, "~> 7.0"}, 9 | {rebar3_ex_doc, "~> 0.2"}, 10 | {rebar3_lint, "~> 3.2"}, 11 | {erlfmt, "~> 1.6"} 12 | ]}. 13 | 14 | {profiles, [ 15 | {test, [ 16 | {erl_opts, []}, 17 | {deps, [ 18 | {proper, "1.5.0"} 19 | ]}, 20 | {plugins, [ 21 | {rebar3_codecov, "0.7.0"} 22 | ]}, 23 | {cover_enabled, true}, 24 | {cover_export_enabled, true} 25 | ]}, 26 | {prod, [ 27 | {erl_opts, [inline_list_funcs, deterministic]} 28 | ]} 29 | ]}. 30 | 31 | {erlfmt, [ 32 | write, 33 | {files, [ 34 | "src/*.{hrl,erl,app.src}", 35 | "test/*.{hrl,erl,app.src}", 36 | "rebar.config" 37 | ]} 38 | ]}. 39 | 40 | {elvis, [ 41 | #{ 42 | dirs => ["src/**"], 43 | filter => "*.erl", 44 | ruleset => erl_files, 45 | rules => [ 46 | {elvis_style, private_data_types, disable}, 47 | {elvis_style, atom_naming_convention, #{ 48 | regex => "^([a-z][a-zA-Z0-9_]*_?)*$" 49 | }} 50 | ] 51 | }, 52 | #{ 53 | dirs => ["."], 54 | filter => "rebar.config", 55 | ruleset => rebar_config 56 | }, 57 | #{ 58 | dirs => ["src/**"], 59 | filter => "*.hrl", 60 | ruleset => hrl_files 61 | } 62 | ]}. 63 | 64 | {hex, [ 65 | {doc, #{provider => ex_doc}} 66 | ]}. 67 | {ex_doc, [ 68 | {source_url, <<"https://github.com/esl/fast_scram">>}, 69 | {main, <<"readme">>}, 70 | {extras, [ 71 | {'README.md', #{title => <<"README">>}}, 72 | {'LICENSE', #{title => <<"License">>}} 73 | ]}, 74 | {main, <<"readme">>} 75 | ]}. 76 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"fast_pbkdf2">>,{pkg,<<"fast_pbkdf2">>,<<"2.0.0">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"fast_pbkdf2">>, <<"72CDEE3C10C6B9B40E31194DE946A883CEEF6CF1F37D7FC9FD1A9D87502723F5">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"fast_pbkdf2">>, <<"74159FD09FB8BF5E97D25137C6C83C28E2CF7E97D7C127D83310DFD0904BD732">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/fast_scram.app.src: -------------------------------------------------------------------------------- 1 | {application, fast_scram, [ 2 | {description, "A fast Salted Challenge Response Authentication Mechanism"}, 3 | {vsn, git}, 4 | {registered, []}, 5 | {applications, [ 6 | kernel, 7 | stdlib, 8 | crypto, 9 | fast_pbkdf2 10 | ]}, 11 | {optional_applications, [fast_pbkdf2]}, 12 | {env, []}, 13 | {modules, []}, 14 | {licenses, ["Apache-2.0"]}, 15 | {links, [{"GitHub", "https://github.com/esl/fast_scram/"}]} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/fast_scram.erl: -------------------------------------------------------------------------------- 1 | %%% @doc SCRAM implementation, see the `README' for details. 2 | -module(fast_scram). 3 | 4 | -include("fast_scram.hrl"). 5 | 6 | -ifdef(WITHOUT_NIFS). 7 | -type sha_type() :: crypto:sha1() | crypto:sha2(). 8 | -else. 9 | -type sha_type() :: crypto:sha1() | crypto:sha2() | sha3_224 | sha3_256 | sha3_384 | sha3_512. 10 | -endif. 11 | %%% Supported underlying hashing algorithms. 12 | -type configuration() :: #{ 13 | entity := client | server, 14 | hash_method := sha_type(), 15 | _ => _ 16 | }. 17 | %%% Configuration for SCRAM, see the `README' for details. 18 | -type username() :: binary(). 19 | %%% Username for the algorithm. 20 | %%% 21 | %%% Required for a client. 22 | -type retrieve_mechanism() :: 23 | fun((username()) -> configuration()) 24 | | fun((username(), state()) -> {configuration(), state()}). 25 | %%% Callback to extract the configuration given a username. 26 | %%% 27 | %%% Required for the server. 28 | -type auth_keys() :: password | salted_password | client_key | stored_key | server_key. 29 | -type auth_data() :: #{auth_keys() => binary()}. 30 | -type plus_variant() :: undefined | none | binary(). 31 | -type nonce() :: #nonce{}. 32 | %%% See `c-nonce' and `s-nonce' at 33 | %%% [https://datatracker.ietf.org/doc/html/rfc5802#section-7]. 34 | -type challenge() :: #challenge{}. 35 | -type channel_binding() :: #channel_binding{}. 36 | -type definitions() :: #scram_definitions{}. 37 | -type state() :: #fast_scram_state{}. 38 | -type next_message() :: binary(). 39 | -type error_message() :: binary(). 40 | %%% See `server-error-message' at 41 | %%% [https://datatracker.ietf.org/doc/html/rfc5802#section-7]. 42 | -type final_message() :: binary(). 43 | %%% See `client-final-message' and `server-final-message' at 44 | %%% [https://datatracker.ietf.org/doc/html/rfc5802#section-7]. 45 | 46 | -export_type([ 47 | sha_type/0, 48 | auth_keys/0, 49 | auth_data/0, 50 | challenge/0, 51 | nonce/0, 52 | plus_variant/0, 53 | channel_binding/0, 54 | retrieve_mechanism/0, 55 | definitions/0, 56 | configuration/0, 57 | next_message/0, 58 | final_message/0, 59 | error_message/0, 60 | state/0 61 | ]). 62 | 63 | -export([hi/4]). 64 | 65 | -export([ 66 | mech_new/1, 67 | mech_step/2 68 | ]). 69 | 70 | -export([ 71 | mech_get/2, 72 | mech_get/3, 73 | mech_set/3 74 | ]). 75 | 76 | -export([ 77 | salted_password/4, 78 | client_key/2, 79 | stored_key/2, 80 | client_signature/3, 81 | client_proof/2, 82 | server_key/2, 83 | server_signature/3 84 | ]). 85 | 86 | -spec mech_new(configuration()) -> 87 | {ok, state()} 88 | | {error, term()}. 89 | mech_new(Config) -> 90 | fast_scram_configuration:mech_new(Config). 91 | 92 | -spec mech_get(term(), state()) -> term(). 93 | mech_get(Key, #fast_scram_state{data = PD}) -> 94 | maps:get(Key, PD, undefined). 95 | 96 | -spec mech_get(term(), state(), term()) -> term(). 97 | mech_get(Key, #fast_scram_state{data = PD}, Default) -> 98 | maps:get(Key, PD, Default). 99 | 100 | -spec mech_set(term(), term(), state()) -> state(). 101 | mech_set(Key, Value, #fast_scram_state{data = PD} = State) -> 102 | State#fast_scram_state{data = PD#{Key => Value}}. 103 | 104 | %%% CLIENT STEPS 105 | -spec mech_step(state(), binary()) -> 106 | {ok, final_message(), state()} 107 | | {continue, next_message(), state()} 108 | | {error, error_message(), state()}. 109 | mech_step( 110 | #fast_scram_state{ 111 | step = 1, 112 | nonce = Nonce, 113 | channel_binding = CbConfig, 114 | data = PrivData 115 | } = State, 116 | <<>> 117 | ) -> 118 | {GS2Header, ClientFirstBare} = fast_scram_attributes:client_first_message( 119 | CbConfig, Nonce, PrivData 120 | ), 121 | ClientFirstMessage = <>, 122 | NewState = fast_scram_parse_rules:append_to_auth_message_in_state(State, ClientFirstBare), 123 | {continue, ClientFirstMessage, NewState#fast_scram_state{step = 3}}; 124 | mech_step(#fast_scram_state{step = 3} = State, ServerIn) -> 125 | case parse_server_first_message(ServerIn, State) of 126 | {ok, 127 | #fast_scram_state{ 128 | nonce = Nonce, 129 | challenge = Challenge, 130 | channel_binding = CbConfig, 131 | scram_definitions = Scram0, 132 | data = PrivData 133 | } = NewState} -> 134 | ClientFinalNoProof = fast_scram_attributes:client_final_message_without_proof( 135 | CbConfig, Nonce, PrivData 136 | ), 137 | Scram1 = fast_scram_parse_rules:append_to_auth_message( 138 | Scram0, <<",", ClientFinalNoProof/binary>> 139 | ), 140 | Scram2 = fast_scram_definitions:scram_definitions_pipe(Scram1, Challenge, PrivData), 141 | ClientProof = Scram2#scram_definitions.client_proof, 142 | ClientFinalMessage = fast_scram_attributes:client_final_message( 143 | ClientFinalNoProof, ClientProof 144 | ), 145 | NewState1 = NewState#fast_scram_state{scram_definitions = Scram2}, 146 | {continue, ClientFinalMessage, NewState1#fast_scram_state{step = 5}}; 147 | {error, Reason} -> 148 | {error, Reason, State} 149 | end; 150 | mech_step(#fast_scram_state{step = 5} = State, ServerIn) -> 151 | case parse_server_final_message(ServerIn, State) of 152 | {ok, NewState} -> 153 | {ok, <<>>, NewState}; 154 | {error, Reason} -> 155 | {error, Reason, State} 156 | end; 157 | %%% SERVER STEPS 158 | %%% An retrieve_mechanism function can add data to the state 159 | %%% For example when the username is needed in order to complete the state data 160 | mech_step(#fast_scram_state{step = 2} = State0, ClientIn) -> 161 | case parse_client_first_message(ClientIn, State0) of 162 | {ok, #fast_scram_state{data = PrivData} = State1} -> 163 | FunRetrieve = maps:get(retrieve_mechanism, PrivData, fun(_) -> #{} end), 164 | case apply_fun(FunRetrieve, State1) of 165 | State2 = #fast_scram_state{} -> 166 | Nonce = State2#fast_scram_state.nonce, 167 | Challenge = State2#fast_scram_state.challenge, 168 | Scram0 = State2#fast_scram_state.scram_definitions, 169 | ServerFirstMsg = fast_scram_attributes:server_first_message( 170 | Nonce, Challenge 171 | ), 172 | Scram1 = fast_scram_definitions:scram_definitions_pipe( 173 | Scram0, Challenge, PrivData 174 | ), 175 | Scram2 = fast_scram_parse_rules:append_to_auth_message( 176 | Scram1, <<",", ServerFirstMsg/binary>> 177 | ), 178 | NewState1 = State2#fast_scram_state{scram_definitions = Scram2}, 179 | {continue, ServerFirstMsg, NewState1#fast_scram_state{step = 4}}; 180 | {error, Reason} -> 181 | {error, Reason, State1} 182 | end; 183 | {error, Reason} -> 184 | {error, Reason, State0} 185 | end; 186 | mech_step(#fast_scram_state{step = 4} = State, ClientIn) -> 187 | case parse_client_final_message(ClientIn, State) of 188 | {ok, 189 | #fast_scram_state{ 190 | challenge = Challenge, 191 | scram_definitions = Scram0, 192 | data = PrivData 193 | } = NewState} -> 194 | GivenClientProof = maps:get(client_proof, PrivData), 195 | Scram = fast_scram_definitions:scram_definitions_pipe(Scram0, Challenge, PrivData), 196 | NewState1 = NewState#fast_scram_state{scram_definitions = Scram}, 197 | case fast_scram_definitions:check_proof(Scram, GivenClientProof) of 198 | ok -> 199 | ServerSignature = Scram#scram_definitions.server_signature, 200 | ServerLastMessage = fast_scram_attributes:server_final_message( 201 | base64:encode(ServerSignature) 202 | ), 203 | {ok, ServerLastMessage, NewState1#fast_scram_state{step = 6}}; 204 | {error, Reason} -> 205 | ServerLastMessage = fast_scram_attributes:server_final_message({error, Reason}), 206 | {error, ServerLastMessage, NewState1} 207 | end; 208 | {error, Reason} -> 209 | ServerLastMessage = fast_scram_attributes:server_final_message({error, Reason}), 210 | {error, ServerLastMessage, State} 211 | end. 212 | 213 | -type username_to_config() :: fun((username()) -> configuration()). 214 | -type username_to_state() :: fun((username(), state()) -> {configuration(), state()}). 215 | 216 | -spec apply_fun(Fun, State) -> Result when 217 | Fun :: username_to_config() | username_to_state(), 218 | State :: state(), 219 | Result :: state() | {error, term()}. 220 | apply_fun(Fun, State) when is_function(Fun, 1) -> 221 | Username = fast_scram:mech_get(username, State), 222 | Result = Fun(Username), 223 | apply_result(Result, State); 224 | apply_fun(Fun, State) when is_function(Fun, 2) -> 225 | Username = fast_scram:mech_get(username, State), 226 | Result = Fun(Username, State), 227 | apply_result(Result, State). 228 | 229 | apply_result({#{} = Config, #fast_scram_state{} = State}, _) -> 230 | fast_scram_configuration:mech_append(State, Config); 231 | apply_result(#{} = Config, State) -> 232 | fast_scram_configuration:mech_append(State, Config); 233 | apply_result({error, Reason}, _) -> 234 | {error, Reason}. 235 | 236 | %%%=================================================================== 237 | %%% SCRAM parsing full messages 238 | %%%=================================================================== 239 | % client-first-message = 240 | % gs2-cbind-flag "," [authzid] "," [reserved-mext ","] username "," nonce ["," extensions] 241 | -spec parse_client_first_message(binary(), state()) -> fast_scram_parse_rules:parse_return(). 242 | parse_client_first_message(ClientIn, State0) -> 243 | Rules = [ 244 | fun fast_scram_parse_rules:parse_gs2_cbind_flag/2, 245 | fun fast_scram_parse_rules:parse_authzid/2, 246 | fun fast_scram_parse_rules:parse_reserved_mext/2, 247 | fun fast_scram_parse_rules:parse_username/2, 248 | fun fast_scram_parse_rules:parse_nonce/2, 249 | fun fast_scram_parse_rules:parse_extensions/2 250 | ], 251 | Chunks = binary:split(ClientIn, <<",">>, [global]), 252 | case match_rules(Chunks, Rules, State0) of 253 | {UnusedRules, State1 = #fast_scram_state{}} when 254 | is_list(UnusedRules), length(UnusedRules) == 1 255 | -> 256 | ClientFirstMsgBare = extract_client_first_msg_bare_from_first(Chunks, ClientIn), 257 | State2 = fast_scram_parse_rules:append_to_auth_message_in_state( 258 | State1, ClientFirstMsgBare 259 | ), 260 | {ok, State2}; 261 | {error, Reason} -> 262 | {error, Reason} 263 | end. 264 | 265 | % server-first-message = 266 | % [reserved-mext ","] nonce "," salt "," 267 | % iteration-count ["," extensions] 268 | -spec parse_server_first_message(binary(), state()) -> fast_scram_parse_rules:parse_return(). 269 | parse_server_first_message(ServerIn, State0) -> 270 | Rules = [ 271 | fun fast_scram_parse_rules:parse_reserved_mext/2, 272 | fun fast_scram_parse_rules:parse_nonce/2, 273 | fun fast_scram_parse_rules:parse_salt/2, 274 | fun fast_scram_parse_rules:parse_iteration_count/2, 275 | fun fast_scram_parse_rules:parse_extensions/2 276 | ], 277 | ServerInChunks = binary:split(ServerIn, <<",">>, [global]), 278 | case match_rules(ServerInChunks, Rules, State0) of 279 | {UnusedRules, State1 = #fast_scram_state{}} when 280 | is_list(UnusedRules), length(UnusedRules) == 1 281 | -> 282 | State2 = fast_scram_parse_rules:append_to_auth_message_in_state( 283 | State1, <<",", ServerIn/binary>> 284 | ), 285 | {ok, State2}; 286 | {error, Reason} -> 287 | {error, Reason} 288 | end. 289 | 290 | % client-final-message = 291 | % channel-binding "," nonce ["," extensions] "," proof 292 | -spec parse_client_final_message(binary(), state()) -> fast_scram_parse_rules:parse_return(). 293 | parse_client_final_message(ClientIn, State0) -> 294 | Rules = [ 295 | fun fast_scram_parse_rules:parse_channel_binding/2, 296 | fun fast_scram_parse_rules:parse_nonce/2, 297 | fun fast_scram_parse_rules:parse_extensions/2, 298 | fun fast_scram_parse_rules:parse_proof/2 299 | ], 300 | ClientInList = binary:split(ClientIn, <<",">>, [global]), 301 | case match_rules(ClientInList, Rules, State0) of 302 | {UnusedRules, State1 = #fast_scram_state{}} when 303 | is_list(UnusedRules), length(UnusedRules) == 0 304 | -> 305 | ClientFinalNoProof = extract_client_final_no_proof(ClientIn), 306 | State2 = fast_scram_parse_rules:append_to_auth_message_in_state( 307 | State1, <<",", ClientFinalNoProof/binary>> 308 | ), 309 | {ok, State2}; 310 | {error, Reason} -> 311 | {error, Reason}; 312 | {_, #fast_scram_state{}} -> 313 | {error, <<"other-error">>} 314 | end. 315 | 316 | % server-final-message = (server-error / verifier) 317 | % ["," extensions] 318 | -spec parse_server_final_message(binary(), state()) -> fast_scram_parse_rules:parse_return(). 319 | parse_server_final_message(ServerIn, State0) -> 320 | Rules = [ 321 | fun fast_scram_parse_rules:parse_server_error_or_verifier/2, 322 | fun fast_scram_parse_rules:parse_extensions/2 323 | ], 324 | ServerInChunks = binary:split(ServerIn, <<",">>, [global]), 325 | case match_rules(ServerInChunks, Rules, State0) of 326 | {UnusedRules, State1 = #fast_scram_state{}} when 327 | is_list(UnusedRules), length(UnusedRules) == 1 328 | -> 329 | {ok, State1}; 330 | {error, Reason} -> 331 | {error, Reason} 332 | end. 333 | 334 | extract_client_first_msg_bare_from_first([Gs2BindFlag, AuthZID | _], ClientIn) -> 335 | NStart = byte_size(Gs2BindFlag) + byte_size(AuthZID) + 2, 336 | binary:part(ClientIn, {NStart, byte_size(ClientIn) - NStart}). 337 | 338 | extract_client_final_no_proof(ClientIn) -> 339 | {PStart, _} = binary:match(ClientIn, <<",p=">>), 340 | binary:part(ClientIn, {0, PStart}). 341 | 342 | %%%=================================================================== 343 | %%% Match parsing rules 344 | %%%=================================================================== 345 | match_rules(Inputs, Rules, State) -> 346 | lists:foldl( 347 | fun 348 | (_, {error, Reason}) -> 349 | {error, Reason}; 350 | (Input, {RulesLeft, St}) -> 351 | apply_rules_until_match(Input, RulesLeft, St) 352 | end, 353 | {Rules, State}, 354 | Inputs 355 | ). 356 | 357 | apply_rules_until_match(_, [], _) -> 358 | {error, <<"error-too-much-input">>}; 359 | apply_rules_until_match(Input, [Rule | RulesLeft], State) -> 360 | case Rule(Input, State) of 361 | {ok, NewState} -> 362 | {RulesLeft, NewState}; 363 | {skip_rule, State} -> 364 | apply_rules_until_match(Input, RulesLeft, State); 365 | {error, Reason} -> 366 | {error, Reason} 367 | end. 368 | 369 | %%%=================================================================== 370 | %%% Expose definitions from internal modules 371 | %%%=================================================================== 372 | -spec salted_password(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). 373 | salted_password(Sha, Password, Salt, IterationCount) -> 374 | fast_scram_definitions:salted_password(Sha, Password, Salt, IterationCount). 375 | 376 | -spec client_key(sha_type(), binary()) -> binary(). 377 | client_key(Sha, SaltedPassword) -> 378 | fast_scram_definitions:client_key(Sha, SaltedPassword). 379 | 380 | -spec stored_key(sha_type(), binary()) -> binary(). 381 | stored_key(Sha, ClientKey) -> 382 | fast_scram_definitions:stored_key(Sha, ClientKey). 383 | 384 | -spec client_signature(sha_type(), binary(), binary()) -> binary(). 385 | client_signature(Sha, StoredKey, AuthMessage) -> 386 | fast_scram_definitions:client_signature(Sha, StoredKey, AuthMessage). 387 | 388 | -spec client_proof(binary(), binary()) -> binary(). 389 | client_proof(ClientKey, ClientSignature) -> 390 | fast_scram_definitions:client_proof(ClientKey, ClientSignature). 391 | 392 | -spec server_key(sha_type(), binary()) -> binary(). 393 | server_key(Sha, SaltedPassword) -> 394 | fast_scram_definitions:server_key(Sha, SaltedPassword). 395 | 396 | -spec server_signature(sha_type(), binary(), binary()) -> binary(). 397 | server_signature(Sha, ServerKey, AuthMessage) -> 398 | fast_scram_definitions:server_signature(Sha, ServerKey, AuthMessage). 399 | 400 | %%% @doc See `Hi(str, salt, i)' at [https://datatracker.ietf.org/doc/html/rfc5802#section-2.2] 401 | -spec hi(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). 402 | hi(Hash, Password, Salt, IterationCount) -> 403 | fast_scram_definitions:salted_password(Hash, Password, Salt, IterationCount). 404 | -------------------------------------------------------------------------------- /src/fast_scram.hrl: -------------------------------------------------------------------------------- 1 | -include_lib("kernel/include/logger.hrl"). 2 | 3 | -define(IS_POSITIVE_INTEGER(N), is_integer(N) andalso N > 0). 4 | -define(IS_VALID_HASH(H), 5 | H =:= sha orelse 6 | H =:= sha224 orelse H =:= sha256 orelse 7 | H =:= sha384 orelse H =:= sha512 orelse 8 | H =:= sha3_224 orelse H =:= sha3_256 orelse 9 | H =:= sha3_384 orelse H =:= sha3_512 10 | ). 11 | 12 | -record(nonce, { 13 | client = <<>> :: binary(), 14 | server = <<>> :: binary() 15 | }). 16 | 17 | -record(challenge, { 18 | salt = <<>> :: binary(), 19 | it_count = 1 :: pos_integer() 20 | }). 21 | 22 | -record(channel_binding, { 23 | variant = undefined :: fast_scram:plus_variant(), 24 | data = <<>> :: binary() 25 | }). 26 | 27 | -record(scram_definitions, { 28 | hash_method :: fast_scram:sha_type(), 29 | %Hi(Normalize(password), salt, i), 30 | salted_password = <<>> :: binary(), 31 | %HMAC(SaltedPassword, "Client Key"), 32 | client_key = <<>> :: binary(), 33 | %H(ClientKey), 34 | stored_key = <<>> :: binary(), 35 | %client-first-message-bare + "," + 36 | auth_message = <<>> :: binary(), 37 | %server-first-message + "," + 38 | %client-final-message-without-proof 39 | 40 | %HMAC(StoredKey, AuthMessage), 41 | client_signature = <<>> :: binary(), 42 | %ClientKey XOR ClientSignature, 43 | client_proof = <<>> :: binary(), 44 | %HMAC(SaltedPassword, "Server Key"), 45 | server_key = <<>> :: binary(), 46 | %HMAC(ServerKey, AuthMessage) 47 | server_signature = <<>> :: binary() 48 | }). 49 | 50 | -record(fast_scram_state, { 51 | %% Steps 1 & 3 are client, 2 & 4 are server 52 | step :: 1..6, 53 | nonce = #nonce{} :: fast_scram:nonce(), 54 | challenge = #challenge{} :: fast_scram:challenge(), 55 | channel_binding = #channel_binding{} :: fast_scram:channel_binding(), 56 | scram_definitions = #scram_definitions{} :: fast_scram:definitions(), 57 | data = #{} :: map() 58 | }). 59 | -------------------------------------------------------------------------------- /src/fast_scram_attributes.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | %% @see fast_scram 3 | -module(fast_scram_attributes). 4 | 5 | -include("fast_scram.hrl"). 6 | 7 | -export([ 8 | reserved_scram_codes/0, 9 | gs2_header/2, 10 | nonce/1, 11 | cbind_input/2, 12 | client_first_message_bare/2, 13 | client_final_message_without_proof/3, 14 | server_first_message/2, 15 | client_first_message/3, 16 | client_final_message/2, 17 | server_final_message/1 18 | ]). 19 | 20 | %%%=================================================================== 21 | %%% SCRAM attributes 22 | %%%=================================================================== 23 | 24 | % -type scram_attribute() :: 25 | % 'a=' | % auth as a different user 26 | % 'n=' | % username, auth identity 27 | % 'm=' | % reserved for extensibility 28 | % 'r=' | % nonce 29 | % 'c=' | % GS2 header and channel binding data 30 | % 's=' | % base64 salt 31 | % 'i=' | % iteration count 32 | % 'p=' | % base64 ClientProof 33 | % 'v=' | % base64 ServerSignature 34 | % 'e='. % error that occurred during auth 35 | reserved_scram_codes() -> 36 | [ 37 | <<"p">>, 38 | <<"n">>, 39 | <<"r">>, 40 | <<"c">>, 41 | <<"s">>, 42 | <<"i">>, 43 | <<"a">>, 44 | <<"v">>, 45 | <<"e">> 46 | ]. 47 | 48 | % gs2-cbind-flag = ("p=" cb-name) / "n" / "y" 49 | % ;; "n" -> client doesn't support channel binding. 50 | % ;; "y" -> client does support channel binding 51 | % ;; but thinks the server does not. 52 | % ;; "p" -> client requires channel binding. 53 | % ;; The selected channel binding follows "p=". 54 | gs2_cbind_flag(undefined) -> 55 | <<"n">>; 56 | gs2_cbind_flag(none) -> 57 | <<"y">>; 58 | gs2_cbind_flag(CB) when is_binary(CB) -> 59 | <<"p=", CB/binary>>. 60 | 61 | % authzid = "a=" saslname 62 | % ;; Protocol specific. 63 | authzid(ID) when is_binary(ID) -> 64 | ID. 65 | 66 | % gs2-header = gs2-cbind-flag "," [ authzid ] "," 67 | % ;; GS2 header for SCRAM 68 | % ;; (the actual GS2 header includes an optional 69 | % ;; flag to indicate that the GSS mechanism is not 70 | % ;; "standard", but since SCRAM is "standard", we 71 | % ;; don't include that flag). 72 | gs2_header(#channel_binding{variant = Variant}, Data) -> 73 | AuthZID = maps:get(auth_zid, Data, <<>>), 74 | <<(gs2_cbind_flag(Variant))/binary, ",", (authzid(AuthZID))/binary, ",">>. 75 | 76 | % reserved-mext = "m=" 1*(value-char) 77 | % ;; Reserved for signaling mandatory extensions. 78 | % ;; The exact syntax will be defined in 79 | % ;; the future. 80 | reserved_mext() -> 81 | <<"">>. 82 | 83 | % username = "n=" saslname 84 | % ;; Usernames are prepared using SASLprep. 85 | username(Username) -> 86 | <<"n=", Username/binary>>. 87 | 88 | % nonce = "r=" c-nonce [s-nonce] 89 | % ;; Second part provided by server. 90 | nonce(#nonce{client = CNonce, server = SNonce}) -> 91 | <<"r=", CNonce/binary, SNonce/binary>>. 92 | 93 | % extensions = attr-val *("," attr-val) 94 | % ;; All extensions are optional, 95 | % ;; i.e., unrecognized attributes 96 | % ;; not defined in this document 97 | % ;; MUST be ignored. 98 | extensions() -> 99 | <<>>. 100 | 101 | %salt = "s=" base64 102 | salt(Salt) -> 103 | <<"s=", (base64:encode(Salt))/binary>>. 104 | 105 | % iteration-count = "i=" posit-number 106 | % ;; A positive number. 107 | iteration_count(IterationCount) when ?IS_POSITIVE_INTEGER(IterationCount) -> 108 | <<"i=", (integer_to_binary(IterationCount))/binary>>. 109 | 110 | % channel-binding = "c=" base64 111 | % ;; base64 encoding of cbind-input. 112 | channel_binding(#channel_binding{data = CBindData} = CBindConfig, Data) -> 113 | <<"c=", (cbind_input(gs2_header(CBindConfig, Data), CBindData))/binary>>. 114 | 115 | cbind_input(GS2Header, CBindData) -> 116 | base64:encode(<>). 117 | 118 | % proof = "p=" base64 119 | proof(Proof) -> 120 | <<"p=", (base64:encode(Proof))/binary>>. 121 | 122 | %%%=================================================================== 123 | %%% SCRAM Messages 124 | %%%=================================================================== 125 | % client-first-message-bare = 126 | % [reserved-mext ","] 127 | % username "," nonce ["," extensions] 128 | -spec client_first_message_bare(map(), fast_scram:nonce()) -> binary(). 129 | client_first_message_bare(#{username := Username}, Nonce) -> 130 | << 131 | (reserved_mext())/binary, 132 | (username(Username))/binary, 133 | ",", 134 | (nonce(Nonce))/binary, 135 | (extensions())/binary 136 | >>. 137 | 138 | % client-final-message-without-proof = 139 | % channel-binding "," nonce ["," extensions] 140 | client_final_message_without_proof(CbConfig, Nonce, Data) -> 141 | <<(channel_binding(CbConfig, Data))/binary, ",", (nonce(Nonce))/binary, (extensions())/binary>>. 142 | 143 | % client-first-message = 144 | % gs2-header client-first-message-bare 145 | -spec client_first_message(fast_scram:channel_binding(), fast_scram:nonce(), map()) -> 146 | {binary(), binary()}. 147 | client_first_message(CbConfig, Nonce, Data) -> 148 | GS2Header = gs2_header(CbConfig, Data), 149 | ClientFirstMessageBare = client_first_message_bare(Data, Nonce), 150 | {GS2Header, ClientFirstMessageBare}. 151 | 152 | % server-first-message = 153 | % [reserved-mext ","] nonce "," salt "," 154 | % iteration-count ["," extensions] 155 | server_first_message(Nonce, #challenge{salt = Salt, it_count = IterationCount}) -> 156 | << 157 | (reserved_mext())/binary, 158 | (nonce(Nonce))/binary, 159 | ",", 160 | (salt(Salt))/binary, 161 | ",", 162 | (iteration_count(IterationCount))/binary, 163 | (extensions())/binary 164 | >>. 165 | 166 | % client-final-message = 167 | % client-final-message-without-proof "," proof 168 | client_final_message(ClientFinalNoProof, ClientProof) -> 169 | <>. 170 | 171 | % server-error = "e=" server-error-value 172 | 173 | % server-error-value = "invalid-encoding" / 174 | % "extensions-not-supported" / ; unrecognized 'm' value 175 | % "invalid-proof" / 176 | % "channel-bindings-dont-match" / 177 | % "server-does-support-channel-binding" / 178 | % ; server does not support channel binding 179 | % "channel-binding-not-supported" / 180 | % "unsupported-channel-binding-type" / 181 | % "unknown-user" / 182 | % "invalid-username-encoding" / 183 | % ; invalid username encoding (invalid UTF-8 or 184 | % ; SASLprep failed) 185 | % "no-resources" / 186 | % "other-error" / 187 | % server-error-value-ext 188 | % ; Unrecognized errors should be treated as "other-error". 189 | % ; In order to prevent information disclosure, the server 190 | % ; may substitute the real reason with "other-error". 191 | 192 | % server-error-value-ext = value 193 | % ; Additional error reasons added by extensions 194 | % ; to this document. 195 | 196 | % verifier = "v=" base64 197 | % ;; base-64 encoded ServerSignature. 198 | 199 | % server-final-message = (server-error / verifier) 200 | % ["," extensions] 201 | server_final_message({error, Reason}) -> 202 | <<"e=", Reason/binary, (extensions())/binary>>; 203 | server_final_message(Verifier) when is_binary(Verifier) -> 204 | <<"v=", Verifier/binary, (extensions())/binary>>. 205 | -------------------------------------------------------------------------------- /src/fast_scram_configuration.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | %% @see fast_scram 3 | -module(fast_scram_configuration). 4 | 5 | -include("fast_scram.hrl"). 6 | -define(DEFAULT_NONCE_SIZE, 16). 7 | 8 | -export([mech_new/1, mech_append/2]). 9 | 10 | % We first match that the strictly required is available 11 | mech_new( 12 | #{ 13 | entity := client, 14 | hash_method := HashMethod, 15 | username := _, 16 | auth_data := AuthData 17 | } = Config 18 | ) -> 19 | St = #fast_scram_state{ 20 | step = 1, scram_definitions = #scram_definitions{hash_method = HashMethod} 21 | }, 22 | Res = build_state(St, AuthData, Config), 23 | maybe_tag_ok(Res); 24 | mech_new( 25 | #{ 26 | entity := server, 27 | hash_method := HashMethod, 28 | retrieve_mechanism := Fun 29 | } = Config 30 | ) when 31 | is_function(Fun, 1); is_function(Fun, 2) 32 | -> 33 | St = #fast_scram_state{ 34 | step = 2, scram_definitions = #scram_definitions{hash_method = HashMethod} 35 | }, 36 | Config1 = ensure_full_config(St, Config), 37 | Res = maps:fold(fun set_val_in_state/3, St, Config1), 38 | maybe_tag_ok(Res); 39 | mech_new(_) -> 40 | {error, <<"Wrong configuration">>}. 41 | 42 | -spec maybe_tag_ok 43 | (fast_scram:state()) -> {ok, fast_scram:state()}; 44 | ({error, T1, T2}) -> {error, T1, T2} when 45 | T1 :: term(), T2 :: term(). 46 | maybe_tag_ok(#fast_scram_state{} = St) -> 47 | {ok, St}; 48 | maybe_tag_ok(Error) -> 49 | Error. 50 | 51 | -spec mech_append(fast_scram:state(), fast_scram:configuration()) -> 52 | fast_scram:state() | {error, binary()}. 53 | mech_append( 54 | #fast_scram_state{step = 2} = St, #{it_count := _, salt := _, auth_data := AuthData} = Config 55 | ) -> 56 | build_state(St, AuthData, Config); 57 | mech_append(_, _) -> 58 | {error, <<"Wrong configuration">>}. 59 | 60 | build_state(St, AuthData, Config) -> 61 | case verify_mandatory_scram_data(maps:keys(AuthData)) of 62 | true -> 63 | Config1 = ensure_full_config(St, Config), 64 | ToFoldThrough = maps:merge(AuthData, maps:without([auth_data], Config1)), 65 | maps:fold(fun set_val_in_state/3, St, ToFoldThrough); 66 | false -> 67 | {error, <<"Invalid authentication configuration">>} 68 | end. 69 | 70 | ensure_full_config(#fast_scram_state{nonce = #nonce{client = C, server = S}}, Config) when 71 | C == <<>>, S == <<>> 72 | -> 73 | case (not maps:is_key(nonce_size, Config) andalso not maps:is_key(nonce, Config)) of 74 | true -> Config#{nonce_size => ?DEFAULT_NONCE_SIZE}; 75 | _ -> Config 76 | end; 77 | ensure_full_config(#fast_scram_state{}, Config) -> 78 | Config. 79 | 80 | % It will get just a combination of the given atoms and verify that they are the exact one 81 | % Correct combinations: 82 | % password alone 83 | % For all other methods, a cached challenge together with a password 84 | % could be given for verification. If a cached challenge is available, 85 | % we first verify if it matches the one given by the server 86 | % salted_password 87 | % stored_key & server_key 88 | % client_key & server_key 89 | -spec verify_mandatory_scram_data([fast_scram:auth_keys()]) -> boolean(). 90 | verify_mandatory_scram_data(List) -> 91 | case lists:sort(List) of 92 | [password] -> true; 93 | [salted_password] -> true; 94 | [password, salted_password] -> true; 95 | [password, server_key, stored_key] -> true; 96 | [client_key, password, server_key] -> true; 97 | [client_key, server_key] -> true; 98 | [server_key, stored_key] -> true; 99 | _ -> false 100 | end. 101 | 102 | %% @doc This only adds a key into the state, verifying typeness, but not if it is repeated. 103 | -type option() :: atom(). 104 | -type value() :: term(). 105 | -spec set_val_in_state(option(), value(), fast_scram:state()) -> 106 | fast_scram:state() | {error, atom(), term()}. 107 | set_val_in_state(entity, Ent, #fast_scram_state{} = St) when is_atom(Ent) -> 108 | case Ent of 109 | client -> St#fast_scram_state{step = 1}; 110 | server -> St#fast_scram_state{step = 2} 111 | end; 112 | set_val_in_state(nonce_size, Num, #fast_scram_state{step = 1} = St) when 113 | ?IS_POSITIVE_INTEGER(Num) 114 | -> 115 | Bin = base64:encode(crypto:strong_rand_bytes(Num)), 116 | St#fast_scram_state{nonce = #nonce{client = Bin}}; 117 | set_val_in_state(nonce_size, Num, #fast_scram_state{step = 2} = St) when 118 | ?IS_POSITIVE_INTEGER(Num) 119 | -> 120 | Bin = base64:encode(crypto:strong_rand_bytes(Num)), 121 | St#fast_scram_state{nonce = #nonce{server = Bin}}; 122 | set_val_in_state(nonce, Bin, #fast_scram_state{step = 1} = St) when is_binary(Bin) -> 123 | St#fast_scram_state{nonce = #nonce{client = Bin}}; 124 | set_val_in_state(nonce, Bin, #fast_scram_state{step = 2} = St) when is_binary(Bin) -> 125 | St#fast_scram_state{nonce = #nonce{server = Bin}}; 126 | set_val_in_state(it_count, Num, #fast_scram_state{challenge = Ch} = St) when 127 | ?IS_POSITIVE_INTEGER(Num) 128 | -> 129 | St#fast_scram_state{challenge = Ch#challenge{it_count = Num}}; 130 | set_val_in_state(salt, Bin, #fast_scram_state{challenge = Ch} = St) when is_binary(Bin) -> 131 | St#fast_scram_state{challenge = Ch#challenge{salt = Bin}}; 132 | set_val_in_state(channel_binding, {Type, Data}, #fast_scram_state{channel_binding = CB} = St) when 133 | is_atom(Type) orelse is_binary(Type), is_binary(Data) 134 | -> 135 | St#fast_scram_state{channel_binding = CB#channel_binding{variant = Type, data = Data}}; 136 | set_val_in_state(hash_method, HM, #fast_scram_state{scram_definitions = SD} = St) when 137 | ?IS_VALID_HASH(HM) 138 | -> 139 | St#fast_scram_state{scram_definitions = SD#scram_definitions{hash_method = HM}}; 140 | set_val_in_state(salted_password, Bin, #fast_scram_state{scram_definitions = SD} = St) when 141 | is_binary(Bin) 142 | -> 143 | St#fast_scram_state{scram_definitions = SD#scram_definitions{salted_password = Bin}}; 144 | set_val_in_state(client_key, Bin, #fast_scram_state{scram_definitions = SD} = St) when 145 | is_binary(Bin) 146 | -> 147 | St#fast_scram_state{scram_definitions = SD#scram_definitions{client_key = Bin}}; 148 | set_val_in_state(stored_key, Bin, #fast_scram_state{scram_definitions = SD} = St) when 149 | is_binary(Bin) 150 | -> 151 | St#fast_scram_state{scram_definitions = SD#scram_definitions{stored_key = Bin}}; 152 | set_val_in_state(client_signature, Bin, #fast_scram_state{scram_definitions = SD} = St) when 153 | is_binary(Bin) 154 | -> 155 | St#fast_scram_state{scram_definitions = SD#scram_definitions{client_signature = Bin}}; 156 | set_val_in_state(client_proof, Bin, #fast_scram_state{scram_definitions = SD} = St) when 157 | is_binary(Bin) 158 | -> 159 | St#fast_scram_state{scram_definitions = SD#scram_definitions{client_proof = Bin}}; 160 | set_val_in_state(server_key, Bin, #fast_scram_state{scram_definitions = SD} = St) when 161 | is_binary(Bin) 162 | -> 163 | St#fast_scram_state{scram_definitions = SD#scram_definitions{server_key = Bin}}; 164 | set_val_in_state(server_signature, Bin, #fast_scram_state{scram_definitions = SD} = St) when 165 | is_binary(Bin) 166 | -> 167 | St#fast_scram_state{scram_definitions = SD#scram_definitions{server_signature = Bin}}; 168 | %% Stuff to data 169 | set_val_in_state(username, UN, #fast_scram_state{data = PD} = St) when is_binary(UN) -> 170 | St#fast_scram_state{data = PD#{username => UN}}; 171 | set_val_in_state(password, PW, #fast_scram_state{data = PD} = St) when is_binary(PW) -> 172 | St#fast_scram_state{data = PD#{password => PW}}; 173 | set_val_in_state(cached_challenge, {It, Salt}, #fast_scram_state{data = PD} = St) when 174 | ?IS_POSITIVE_INTEGER(It), is_binary(Salt) 175 | -> 176 | Challenge = #challenge{it_count = It, salt = Salt}, 177 | St#fast_scram_state{data = PD#{challenge => Challenge}}; 178 | set_val_in_state(cached_challenge, {Salt, It}, #fast_scram_state{data = PD} = St) when 179 | ?IS_POSITIVE_INTEGER(It), is_binary(Salt) 180 | -> 181 | Challenge = #challenge{it_count = It, salt = Salt}, 182 | St#fast_scram_state{data = PD#{challenge => Challenge}}; 183 | set_val_in_state(retrieve_mechanism, Fun, #fast_scram_state{data = PD} = St) when 184 | is_function(Fun, 1); is_function(Fun, 2) 185 | -> 186 | St#fast_scram_state{data = PD#{retrieve_mechanism => Fun}}; 187 | set_val_in_state(WrongKey, _, #fast_scram_state{}) -> 188 | {error, wrong_key, WrongKey}; 189 | set_val_in_state(_, _, {error, wrong_key, _} = Error) -> 190 | Error. 191 | -------------------------------------------------------------------------------- /src/fast_scram_definitions.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | %% @see fast_scram 3 | -module(fast_scram_definitions). 4 | 5 | -include("fast_scram.hrl"). 6 | 7 | -export([ 8 | salted_password/4, 9 | client_key/2, 10 | stored_key/2, 11 | client_signature/3, 12 | client_proof/2, 13 | server_key/2, 14 | server_signature/3 15 | ]). 16 | 17 | -export([ 18 | scram_definitions_pipe/3, 19 | check_proof/2 20 | ]). 21 | 22 | %%%=================================================================== 23 | %%% SCRAM Definitions 24 | %%%=================================================================== 25 | %% SaltedPassword := Hi(Normalize(password), salt, i) 26 | %% ClientKey := HMAC(SaltedPassword, "Client Key") 27 | %% StoredKey := H(ClientKey) 28 | %% AuthMessage := client-first-message-bare + "," + 29 | %% server-first-message + "," + 30 | %% client-final-message-without-proof 31 | %% ClientSignature := HMAC(StoredKey, AuthMessage) 32 | %% ClientProof := ClientKey XOR ClientSignature 33 | %% ServerKey := HMAC(SaltedPassword, "Server Key") 34 | %% ServerSignature := HMAC(ServerKey, AuthMessage) 35 | 36 | -ifdef(WITHOUT_NIFS). 37 | -spec salted_password(fast_scram:sha_type(), binary(), binary(), non_neg_integer()) -> binary(). 38 | salted_password(Sha, Password, Salt, IterationCount) when 39 | ?IS_VALID_HASH(Sha), is_binary(Password), is_binary(Salt), ?IS_POSITIVE_INTEGER(IterationCount) 40 | -> 41 | #{size := KeyLength} = crypto:hash_info(Sha), 42 | crypto:pbkdf2_hmac(Sha, Password, Salt, IterationCount, KeyLength). 43 | -else. 44 | -spec salted_password(fast_scram:sha_type(), binary(), binary(), non_neg_integer()) -> binary(). 45 | salted_password(Sha, Password, Salt, IterationCount) when 46 | ?IS_VALID_HASH(Sha), is_binary(Password), is_binary(Salt), ?IS_POSITIVE_INTEGER(IterationCount) 47 | -> 48 | fast_pbkdf2:pbkdf2(Sha, Password, Salt, IterationCount). 49 | -endif. 50 | 51 | -spec client_key(fast_scram:sha_type(), binary()) -> binary(). 52 | client_key(Sha, SaltedPassword) when 53 | ?IS_VALID_HASH(Sha), is_binary(SaltedPassword) 54 | -> 55 | crypto_hmac(Sha, SaltedPassword, <<"Client Key">>). 56 | 57 | -spec stored_key(fast_scram:sha_type(), binary()) -> binary(). 58 | stored_key(Sha, ClientKey) when 59 | ?IS_VALID_HASH(Sha), is_binary(ClientKey) 60 | -> 61 | crypto:hash(Sha, ClientKey). 62 | 63 | -spec client_signature(fast_scram:sha_type(), binary(), binary()) -> binary(). 64 | client_signature(Sha, StoredKey, AuthMessage) when 65 | ?IS_VALID_HASH(Sha), is_binary(StoredKey), is_binary(AuthMessage) 66 | -> 67 | crypto_hmac(Sha, StoredKey, AuthMessage). 68 | 69 | -spec client_proof(binary(), binary()) -> binary(). 70 | client_proof(ClientKey, ClientSignature) when 71 | is_binary(ClientKey), is_binary(ClientSignature) 72 | -> 73 | crypto:exor(ClientKey, ClientSignature). 74 | 75 | -spec server_key(fast_scram:sha_type(), binary()) -> binary(). 76 | server_key(Sha, SaltedPassword) when 77 | ?IS_VALID_HASH(Sha), is_binary(SaltedPassword) 78 | -> 79 | crypto_hmac(Sha, SaltedPassword, <<"Server Key">>). 80 | 81 | -spec server_signature(fast_scram:sha_type(), binary(), binary()) -> binary(). 82 | server_signature(Sha, ServerKey, AuthMessage) when 83 | ?IS_VALID_HASH(Sha), is_binary(ServerKey), is_binary(AuthMessage) 84 | -> 85 | crypto_hmac(Sha, ServerKey, AuthMessage). 86 | 87 | -ifdef(OTP_RELEASE). 88 | -if(?OTP_RELEASE >= 23). 89 | crypto_hmac(Sha, Bin1, Bin2) -> 90 | crypto:mac(hmac, Sha, Bin1, Bin2). 91 | -else. 92 | crypto_hmac(Sha, Bin1, Bin2) -> 93 | crypto:hmac(Sha, Bin1, Bin2). 94 | -endif. 95 | -else. 96 | crypto_hmac(Sha, Bin1, Bin2) -> 97 | crypto:hmac(Sha, Bin1, Bin2). 98 | -endif. 99 | 100 | -spec scram_definitions_pipe(fast_scram:definitions(), fast_scram:challenge(), map()) -> 101 | fast_scram:definitions(). 102 | %% Typical scenario, auth_message is full and we only have the plain password 103 | scram_definitions_pipe( 104 | #scram_definitions{ 105 | hash_method = HashMethod, 106 | salted_password = <<>>, 107 | auth_message = AuthMessage 108 | } = Scram, 109 | #challenge{salt = Salt, it_count = ItCount}, 110 | #{password := Password} 111 | ) when AuthMessage =/= <<>> -> 112 | SaltedPassword = salted_password(HashMethod, Password, Salt, ItCount), 113 | ClientKey = client_key(HashMethod, SaltedPassword), 114 | StoredKey = stored_key(HashMethod, ClientKey), 115 | ClientSignature = client_signature(HashMethod, StoredKey, AuthMessage), 116 | ClientProof = client_proof(ClientKey, ClientSignature), 117 | ServerKey = server_key(HashMethod, SaltedPassword), 118 | ServerSignature = server_signature(HashMethod, ServerKey, AuthMessage), 119 | Scram#scram_definitions{ 120 | salted_password = SaltedPassword, 121 | client_key = ClientKey, 122 | stored_key = StoredKey, 123 | auth_message = AuthMessage, 124 | client_signature = ClientSignature, 125 | client_proof = ClientProof, 126 | server_key = ServerKey, 127 | server_signature = ServerSignature 128 | }; 129 | %% We have a cached challenge, we need to verify if it is correct 130 | scram_definitions_pipe( 131 | #scram_definitions{hash_method = HashMethod, auth_message = AuthMessage} = Scram, 132 | #challenge{} = GivenChallenge, 133 | #{challenge := StoredChallenge} = Data 134 | ) -> 135 | case GivenChallenge =:= StoredChallenge of 136 | % This means that the client has cached the challenge correctly 137 | true -> 138 | partial_compute(Scram); 139 | % Invalid cache, remove all knowledge and try again 140 | false -> 141 | ScramWithoutCached = #scram_definitions{ 142 | hash_method = HashMethod, auth_message = AuthMessage 143 | }, 144 | DataWithoutCached = maps:remove(challenge, Data), 145 | scram_definitions_pipe(ScramWithoutCached, GivenChallenge, DataWithoutCached) 146 | end; 147 | scram_definitions_pipe(Scram, _, _) -> 148 | partial_compute(Scram). 149 | 150 | partial_compute( 151 | #scram_definitions{ 152 | hash_method = HashMethod, 153 | auth_message = AuthMessage, 154 | client_key = ClientKey, 155 | server_key = ServerKey 156 | } = Scram 157 | ) when ClientKey =/= <<>>, ServerKey =/= <<>> -> 158 | StoredKey = stored_key(HashMethod, ClientKey), 159 | ClientSignature = client_signature(HashMethod, StoredKey, AuthMessage), 160 | ClientProof = client_proof(ClientKey, ClientSignature), 161 | ServerSignature = server_signature(HashMethod, ServerKey, AuthMessage), 162 | Scram#scram_definitions{ 163 | stored_key = StoredKey, 164 | client_signature = ClientSignature, 165 | client_proof = ClientProof, 166 | server_signature = ServerSignature 167 | }; 168 | partial_compute( 169 | #scram_definitions{ 170 | hash_method = HashMethod, 171 | auth_message = AuthMessage, 172 | stored_key = StoredKey, 173 | server_key = ServerKey 174 | } = Scram 175 | ) when StoredKey =/= <<>>, ServerKey =/= <<>> -> 176 | ClientSignature = client_signature(HashMethod, StoredKey, AuthMessage), 177 | ServerSignature = server_signature(HashMethod, ServerKey, AuthMessage), 178 | Scram#scram_definitions{ 179 | client_signature = ClientSignature, 180 | server_signature = ServerSignature 181 | }; 182 | partial_compute( 183 | #scram_definitions{ 184 | hash_method = HashMethod, 185 | auth_message = AuthMessage, 186 | salted_password = SaltedPassword 187 | } = Scram 188 | ) when SaltedPassword =/= <<>> -> 189 | ClientKey = client_key(HashMethod, SaltedPassword), 190 | StoredKey = stored_key(HashMethod, ClientKey), 191 | ClientSignature = client_signature(HashMethod, StoredKey, AuthMessage), 192 | ClientProof = client_proof(ClientKey, ClientSignature), 193 | ServerKey = server_key(HashMethod, SaltedPassword), 194 | ServerSignature = server_signature(HashMethod, ServerKey, AuthMessage), 195 | Scram#scram_definitions{ 196 | client_key = ClientKey, 197 | stored_key = StoredKey, 198 | client_signature = ClientSignature, 199 | client_proof = ClientProof, 200 | server_key = ServerKey, 201 | server_signature = ServerSignature 202 | }; 203 | partial_compute(Scram) -> 204 | ?LOG_DEBUG(#{what => scram_no_pipe_match}), 205 | Scram. 206 | 207 | -spec check_proof(fast_scram:definitions(), binary()) -> ok | {error, binary()}. 208 | check_proof(#scram_definitions{client_proof = CalculatedClientProof}, GivenClientProof) when 209 | CalculatedClientProof =:= GivenClientProof 210 | -> 211 | ok; 212 | check_proof( 213 | #scram_definitions{ 214 | hash_method = HashMethod, 215 | client_proof = <<>>, 216 | stored_key = StoredKey, 217 | client_signature = ClientSignature 218 | }, 219 | GivenClientProof 220 | ) -> 221 | ClientKey = client_proof(GivenClientProof, ClientSignature), 222 | CalculatedStoredKey = stored_key(HashMethod, ClientKey), 223 | case CalculatedStoredKey =:= StoredKey of 224 | true -> ok; 225 | _ -> {error, <<"invalid-proof">>} 226 | end; 227 | check_proof(_, _) -> 228 | {error, <<"invalid-proof">>}. 229 | -------------------------------------------------------------------------------- /src/fast_scram_parse_rules.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | %% @see fast_scram 3 | -module(fast_scram_parse_rules). 4 | 5 | -include("fast_scram.hrl"). 6 | 7 | -type parse_return() :: 8 | {ok, fast_scram:state()} 9 | | {skip_rule, fast_scram:state()} 10 | | {error, binary()}. 11 | -export_type([parse_return/0]). 12 | 13 | -export([ 14 | parse_gs2_cbind_flag/2, 15 | parse_authzid/2, 16 | parse_username/2, 17 | parse_nonce/2, 18 | parse_salt/2, 19 | parse_iteration_count/2, 20 | parse_reserved_mext/2, 21 | parse_extensions/2, 22 | parse_proof/2, 23 | parse_channel_binding/2, 24 | parse_server_error_or_verifier/2 25 | ]). 26 | -export([ 27 | append_to_auth_message/2, 28 | append_to_auth_message_in_state/2 29 | ]). 30 | 31 | -spec parse_gs2_cbind_flag(binary(), fast_scram:state()) -> parse_return(). 32 | parse_gs2_cbind_flag(<<>>, _State) -> 33 | {error, <<"no-resources">>}; 34 | parse_gs2_cbind_flag(CBind, #fast_scram_state{channel_binding = CbConfig} = State) -> 35 | % NOTE: if the client doesn't support channel-binding, remove it from the state 36 | case supported_channel_binding_flag(CBind, CbConfig) of 37 | #channel_binding{} = NewCbConfig -> 38 | {ok, State#fast_scram_state{channel_binding = NewCbConfig}}; 39 | {error, Reason} -> 40 | {error, Reason} 41 | end. 42 | 43 | -spec parse_authzid(binary(), fast_scram:state()) -> parse_return(). 44 | parse_authzid(<<>>, State) -> 45 | {ok, State}; 46 | parse_authzid(<<"a=", _Rest/binary>>, _State) -> 47 | {error, <<"authzid-flag-not-supported">>}; 48 | parse_authzid(_, _State) -> 49 | {error, <<"no-resources">>}. 50 | 51 | % Mandatory extensions sent by one peer but not understood by the 52 | % other MUST cause authentication failure (the server SHOULD send 53 | % the "extensions-not-supported" server-error-value). 54 | % Unknown optional extensions MUST be ignored upon receipt. 55 | -spec parse_reserved_mext(binary(), fast_scram:state()) -> parse_return(). 56 | parse_reserved_mext(<<"m=", _/binary>>, _State) -> 57 | {error, <<"extensions-not-supported">>}; 58 | parse_reserved_mext(_, State) -> 59 | {skip_rule, State}. 60 | 61 | -spec parse_username(binary(), fast_scram:state()) -> parse_return(). 62 | parse_username(<<>>, _State) -> 63 | {error, <<"unknown-user">>}; 64 | parse_username(<<"n=", UnescapedUsername/binary>>, #fast_scram_state{data = Data} = State) -> 65 | case replace_escaped_chars(UnescapedUsername) of 66 | {ok, EscapedUsername} -> 67 | case maps:get(username, Data, undefined) of 68 | CachedName when is_binary(CachedName), CachedName =/= EscapedUsername -> 69 | {error, <<"unknown-user">>}; 70 | _ -> 71 | NewData = Data#{username => EscapedUsername}, 72 | {ok, State#fast_scram_state{data = NewData}} 73 | end; 74 | E -> 75 | E 76 | end; 77 | parse_username(_, _) -> 78 | {error, <<"other-error">>}. 79 | 80 | -spec parse_nonce(binary(), fast_scram:state()) -> parse_return(). 81 | parse_nonce(<<>>, _State) -> 82 | {error, <<"no-resources">>}; 83 | parse_nonce(<<"r=", Nonce/binary>>, State) -> 84 | case update_append_nonce(Nonce, State#fast_scram_state.nonce) of 85 | #nonce{} = N -> 86 | {ok, State#fast_scram_state{nonce = N}}; 87 | {error, _} = E -> 88 | E 89 | end; 90 | parse_nonce(_, _) -> 91 | {error, <<"other-error">>}. 92 | 93 | -spec parse_salt(binary(), fast_scram:state()) -> parse_return(). 94 | parse_salt(<<>>, _State) -> 95 | {error, <<"no-resources">>}; 96 | parse_salt(<<"s=", Salt0/binary>>, State) -> 97 | Challenge = State#fast_scram_state.challenge, 98 | case maybe_base64_decode(Salt0) of 99 | {error, Reason} -> 100 | {error, Reason}; 101 | Salt -> 102 | {ok, State#fast_scram_state{challenge = Challenge#challenge{salt = Salt}}} 103 | end; 104 | parse_salt(_, _) -> 105 | {error, <<"other-error">>}. 106 | 107 | -spec parse_iteration_count(binary(), fast_scram:state()) -> parse_return(). 108 | parse_iteration_count(<<>>, _State) -> 109 | {error, <<"no-resources">>}; 110 | parse_iteration_count(<<"i=", ItCount/binary>>, State) -> 111 | Challenge = State#fast_scram_state.challenge, 112 | try binary_to_integer(ItCount) of 113 | It when ?IS_POSITIVE_INTEGER(It) -> 114 | {ok, State#fast_scram_state{challenge = Challenge#challenge{it_count = It}}} 115 | catch 116 | _:_ -> 117 | {error, <<"invalid-iteration-count">>} 118 | end; 119 | parse_iteration_count(_, _) -> 120 | {error, <<"other-error">>}. 121 | 122 | -spec parse_extensions(binary(), fast_scram:state()) -> parse_return(). 123 | parse_extensions(<<>>, _) -> 124 | {error, <<"other-error">>}; 125 | parse_extensions(<<"m=", _/binary>>, _) -> 126 | {error, <<"extensions-not-supported">>}; 127 | parse_extensions(<>, State) -> 128 | case 129 | lists:any( 130 | fun(El) -> Char =:= El end, 131 | fast_scram_attributes:reserved_scram_codes() 132 | ) 133 | of 134 | true -> {skip_rule, State}; 135 | false -> {error, <<"extensions-not-supported">>} 136 | end. 137 | 138 | -spec parse_proof(binary(), fast_scram:state()) -> parse_return(). 139 | parse_proof(<<>>, _State) -> 140 | {error, <<"no-resources">>}; 141 | parse_proof(<<"p=">>, _State) -> 142 | {error, <<"invalid-proof">>}; 143 | parse_proof( 144 | <<"p=", Proof0/binary>>, 145 | #fast_scram_state{data = Data} = State 146 | ) -> 147 | case maybe_base64_decode(Proof0) of 148 | {error, Reason} -> 149 | {error, Reason}; 150 | Proof -> 151 | {ok, State#fast_scram_state{data = Data#{client_proof => Proof}}} 152 | end. 153 | 154 | -spec parse_channel_binding(binary(), fast_scram:state()) -> parse_return(). 155 | parse_channel_binding(<<>>, _State) -> 156 | {error, <<"no-resources">>}; 157 | parse_channel_binding( 158 | <<"c=", CB/binary>>, 159 | #fast_scram_state{ 160 | channel_binding = CbConfig, 161 | data = Data 162 | } = State 163 | ) -> 164 | case verify_cbind_input(CB, CbConfig, Data) of 165 | ok -> 166 | {ok, State}; 167 | {error, Reason} -> 168 | {error, Reason} 169 | end; 170 | parse_channel_binding(_, _) -> 171 | {error, <<"no-resources">>}. 172 | 173 | -spec parse_server_error_or_verifier(binary(), fast_scram:state()) -> parse_return(). 174 | parse_server_error_or_verifier( 175 | <<"v=", Verifier/binary>>, 176 | #fast_scram_state{scram_definitions = #scram_definitions{} = ScramDefs} = State 177 | ) -> 178 | ServerSignature = ScramDefs#scram_definitions.server_signature, 179 | case maybe_base64_decode(Verifier) of 180 | ServerSignature -> 181 | {ok, State}; 182 | {error, Reason} -> 183 | {error, Reason}; 184 | _ -> 185 | {error, <<"authentication-failure">>} 186 | end; 187 | parse_server_error_or_verifier(<<"e=", Error/binary>>, _State) -> 188 | {error, Error}. 189 | 190 | -spec append_to_auth_message(fast_scram:definitions(), binary()) -> fast_scram:definitions(). 191 | append_to_auth_message(ScramDefs, NewChunk) -> 192 | CurrentAuthMessage = ScramDefs#scram_definitions.auth_message, 193 | NewAppendedAuthMessage = <>, 194 | ScramDefs#scram_definitions{auth_message = NewAppendedAuthMessage}. 195 | 196 | -spec append_to_auth_message_in_state(fast_scram:state(), binary()) -> fast_scram:state(). 197 | append_to_auth_message_in_state(State, NewChunk) -> 198 | ScramDefs0 = State#fast_scram_state.scram_definitions, 199 | ScramDefs1 = append_to_auth_message(ScramDefs0, NewChunk), 200 | State#fast_scram_state{scram_definitions = ScramDefs1}. 201 | 202 | %%-------------------------------------------------------------------- 203 | %% @doc 204 | %% Replace "=2C" with "," and "=3D" with "=". Return invalid-username-encoding 205 | %% if any "=" char is not preceded with either "2C" or "3D". 206 | %% @end 207 | %%-------------------------------------------------------------------- 208 | -spec replace_escaped_chars(binary()) -> {ok, binary()} | {error, binary()}. 209 | replace_escaped_chars(UnescapedUsername) -> 210 | case binary:match(UnescapedUsername, <<"=">>) of 211 | nomatch -> 212 | {ok, UnescapedUsername}; 213 | _ -> 214 | ReplacedEqual = binary:replace(UnescapedUsername, <<"=3D">>, <<"=">>, [global]), 215 | EscapedUsername = binary:replace(ReplacedEqual, <<"=2C">>, <<",">>, [global]), 216 | case binary:match(EscapedUsername, <<"=">>) of 217 | nomatch -> 218 | {ok, EscapedUsername}; 219 | _ -> 220 | {error, <<"invalid-username-encoding">>} 221 | end 222 | end. 223 | 224 | -spec supported_channel_binding_flag(binary(), fast_scram:channel_binding()) -> 225 | fast_scram:channel_binding() | {error, binary()}. 226 | supported_channel_binding_flag( 227 | <<"p=", Type/binary>>, 228 | #channel_binding{variant = Type} = CbConfig 229 | ) when Type =/= undefined -> 230 | CbConfig; 231 | supported_channel_binding_flag( 232 | <<"p=", _Type/binary>>, 233 | #channel_binding{variant = OtherType} 234 | ) when OtherType =/= undefined -> 235 | {error, <<"unsupported-channel-binding-type">>}; 236 | supported_channel_binding_flag( 237 | <<"p=", _Type/binary>>, 238 | #channel_binding{variant = undefined} 239 | ) -> 240 | {error, <<"channel-binding-not-supported">>}; 241 | supported_channel_binding_flag( 242 | <<"y">>, 243 | #channel_binding{variant = undefined} = CbConfig 244 | ) -> 245 | CbConfig; 246 | supported_channel_binding_flag( 247 | <<"y">>, 248 | #channel_binding{variant = Type} 249 | ) when Type =/= undefined -> 250 | {error, <<"server-does-support-channel-binding">>}; 251 | supported_channel_binding_flag( 252 | <<"n">>, 253 | CbConfig 254 | ) -> 255 | CbConfig#channel_binding{variant = undefined}; 256 | supported_channel_binding_flag(_, _) -> 257 | {error, <<"other-error">>}. 258 | 259 | -spec update_append_nonce(binary(), fast_scram:nonce()) -> fast_scram:nonce() | {error, binary()}. 260 | update_append_nonce(ClientNonce, #nonce{client = <<>>} = Nonce) -> 261 | Nonce#nonce{client = ClientNonce}; 262 | update_append_nonce(FullNonce, #nonce{client = Client, server = <<>>} = Nonce) -> 263 | case binary:longest_common_prefix([FullNonce, Client]) of 264 | N when N == byte_size(Client) -> 265 | Nonce#nonce{server = binary:part(FullNonce, {N, byte_size(FullNonce) - N})}; 266 | _ -> 267 | {error, <<"invalid-nonce">>} 268 | end; 269 | update_append_nonce(FullNonce, #nonce{client = Client, server = Server} = Nonce) -> 270 | case FullNonce == <> of 271 | true -> 272 | Nonce; 273 | _ -> 274 | {error, <<"invalid-nonce">>} 275 | end. 276 | 277 | -spec verify_cbind_input(binary(), fast_scram:channel_binding(), map()) -> ok | {error, binary()}. 278 | verify_cbind_input(CBindInput, #channel_binding{data = CBindData} = CbConfig, Data) -> 279 | Constructed = fast_scram_attributes:cbind_input( 280 | fast_scram_attributes:gs2_header(CbConfig, Data), 281 | CBindData 282 | ), 283 | case Constructed =:= CBindInput of 284 | true -> ok; 285 | false -> {error, <<"channel-bindings-dont-match">>} 286 | end. 287 | 288 | maybe_base64_decode(Binary) -> 289 | try base64:decode(Binary) of 290 | Decoded -> Decoded 291 | catch 292 | error:badarg -> 293 | {error, <<"invalid-encoding">>} 294 | end. 295 | -------------------------------------------------------------------------------- /test/scram_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(scram_SUITE). 2 | 3 | -include("src/fast_scram.hrl"). 4 | 5 | %% API 6 | -export([all/0, groups/0]). 7 | 8 | %% test cases 9 | -export([ 10 | regular_scram_authentication_example_from_the_rfc/1, 11 | regular_scram_authentication_sha1/1, 12 | regular_scram_authentication_sha2/1, 13 | regular_scram_authentication_sha3/1, 14 | wrong_configuration_key/1, 15 | configuration_client_sends_wrong_username/1, 16 | configuration_cached_keys_works_easily/0, 17 | configuration_cached_keys_works_easily/1, 18 | configuration_cached_keys_works_easily_v2/0, 19 | configuration_cached_keys_works_easily_v2/1, 20 | configuration_cached_wrong_simply_recalculates/0, 21 | configuration_cached_wrong_simply_recalculates/1, 22 | configuration_cached_wrong_without_password_fails/0, 23 | configuration_cached_wrong_without_password_fails/1, 24 | verification_name_escapes_values_correctly/1, 25 | verification_name_does_not_escape_values_correctly/1, 26 | authentication_server_last_message_is_an_error/1, 27 | authentication_server_rejects_the_proof/1, 28 | authentication_server_rejects_invalid_encoded_proof/1, 29 | authentication_client_rejects_the_signature/1, 30 | nonce_client_receives_invalid/1, 31 | nonce_server_finds_non_matching/1, 32 | channel_not_advertise_but_client_could_is_ok/1, 33 | channel_binding_client_did_not_see_available_plus/1, 34 | channel_server_offers_but_client_does_not_take_is_ok/1, 35 | channel_type_does_not_match/1, 36 | channel_type_matches_but_data_does_not/1, 37 | channel_is_not_supported_by_the_server/1, 38 | missing_username/1, 39 | missing_authzid/1, 40 | missing_gs2/1, 41 | missing_gs2_info/1, 42 | missing_nonce/1, 43 | missing_salt/1, 44 | missing_it_count/1, 45 | missing_proof/1, 46 | missing_proof_info/1, 47 | missing_channel_binding/1, 48 | missing_channel_binding_info/1, 49 | wrong_flag_username/1, 50 | wrong_flag_g2s/1, 51 | wrong_flag_nonce/1, 52 | wrong_flag_salt/1, 53 | wrong_flag_it_count/1, 54 | wrong_it_count/1, 55 | too_much_input/1, 56 | not_supported_authzid/1, 57 | not_supported_mext/1, 58 | not_supported_extension/1 59 | ]). 60 | 61 | -include_lib("stdlib/include/assert.hrl"). 62 | 63 | all() -> 64 | [ 65 | {group, verifications}, 66 | {group, authentication}, 67 | {group, nonce}, 68 | {group, channel}, 69 | {group, missing_flags}, 70 | {group, wrong_input}, 71 | {group, not_supported} 72 | ]. 73 | 74 | groups() -> 75 | [ 76 | {verifications, [parallel], [ 77 | regular_scram_authentication_example_from_the_rfc, 78 | regular_scram_authentication_sha1, 79 | regular_scram_authentication_sha2, 80 | regular_scram_authentication_sha3, 81 | wrong_configuration_key, 82 | verification_name_escapes_values_correctly, 83 | verification_name_does_not_escape_values_correctly, 84 | configuration_client_sends_wrong_username, 85 | configuration_cached_keys_works_easily, 86 | configuration_cached_keys_works_easily_v2, 87 | configuration_cached_wrong_simply_recalculates, 88 | configuration_cached_wrong_without_password_fails 89 | ]}, 90 | {authentication, [parallel], [ 91 | authentication_server_last_message_is_an_error, 92 | authentication_server_rejects_the_proof, 93 | authentication_server_rejects_invalid_encoded_proof, 94 | authentication_client_rejects_the_signature 95 | ]}, 96 | {nonce, [parallel], [ 97 | nonce_client_receives_invalid, 98 | nonce_server_finds_non_matching 99 | ]}, 100 | {channel, [parallel], [ 101 | channel_not_advertise_but_client_could_is_ok, 102 | channel_binding_client_did_not_see_available_plus, 103 | channel_server_offers_but_client_does_not_take_is_ok, 104 | channel_type_does_not_match, 105 | channel_type_matches_but_data_does_not, 106 | channel_is_not_supported_by_the_server 107 | ]}, 108 | {missing_flags, [parallel], [ 109 | missing_username, 110 | missing_authzid, 111 | missing_gs2, 112 | missing_gs2_info, 113 | missing_nonce, 114 | missing_salt, 115 | missing_it_count, 116 | missing_proof, 117 | missing_proof_info, 118 | missing_channel_binding, 119 | missing_channel_binding_info 120 | ]}, 121 | {wrong_input, [], [ 122 | wrong_flag_username, 123 | wrong_flag_g2s, 124 | wrong_flag_nonce, 125 | wrong_flag_salt, 126 | wrong_flag_it_count, 127 | wrong_it_count, 128 | too_much_input 129 | ]}, 130 | {not_supported, [parallel], [ 131 | not_supported_authzid, 132 | not_supported_mext, 133 | not_supported_extension 134 | ]} 135 | ]. 136 | 137 | %%%=================================================================== 138 | %%% Individual Test Cases (from groups() definition) 139 | %%%=================================================================== 140 | regular_scram_authentication_example_from_the_rfc(_Config) -> 141 | %% Client and Server have matching configurations 142 | ClientState1 = typical_scram_configuration(client), 143 | ServerState2 = typical_scram_configuration(server), 144 | %% AUTH 145 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 146 | ?assertEqual(client_first(), ClientFirst), 147 | %% CHALLENGE 148 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 149 | ?assertEqual(server_first(), ServerFirst), 150 | %% RESPONSE 151 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 152 | ?assertEqual(client_final(), ClientFinal), 153 | %% SUCCESS 154 | {ok, ServerFinal, _} = fast_scram:mech_step(ServerState4, ClientFinal), 155 | ?assertEqual(server_final(), ServerFinal), 156 | %% Client successfully accepts the server's verifier 157 | {ok, _Final, _ClientState7} = fast_scram:mech_step(ClientState5, ServerFinal). 158 | 159 | regular_scram_authentication_sha1(_Config) -> 160 | regular_scram_authentication(_Config, sha). 161 | 162 | regular_scram_authentication_sha2(_Config) -> 163 | regular_scram_authentication(_Config, sha224), 164 | regular_scram_authentication(_Config, sha256), 165 | regular_scram_authentication(_Config, sha384), 166 | regular_scram_authentication(_Config, sha512). 167 | 168 | regular_scram_authentication_sha3(_Config) -> 169 | regular_scram_authentication(_Config, sha3_224), 170 | regular_scram_authentication(_Config, sha3_256), 171 | regular_scram_authentication(_Config, sha3_384), 172 | regular_scram_authentication(_Config, sha3_512). 173 | 174 | regular_scram_authentication(_Config, Hash) -> 175 | Password = base64:encode(crypto:strong_rand_bytes(8 + rand:uniform(8))), 176 | {ok, ClientState1} = fast_scram:mech_new(#{ 177 | entity => client, 178 | hash_method => Hash, 179 | username => <<"user">>, 180 | auth_data => #{password => Password} 181 | }), 182 | {ok, ServerState2} = fast_scram:mech_new( 183 | #{ 184 | entity => server, 185 | hash_method => Hash, 186 | username => <<"user">>, 187 | retrieve_mechanism => 188 | fun(U, S) -> retrieve_mechanism(U, #{password => Password}, S) end 189 | } 190 | ), 191 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 192 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 193 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 194 | {ok, ServerFinal, _} = fast_scram:mech_step(ServerState4, ClientFinal), 195 | {ok, _Final, _ClientState7} = fast_scram:mech_step(ClientState5, ServerFinal). 196 | 197 | wrong_configuration_key(_Config) -> 198 | {error, wrong_key, bad_key} = fast_scram:mech_new( 199 | #{ 200 | entity => client, 201 | hash_method => sha256, 202 | username => <<"user">>, 203 | bad_key => any_value, 204 | auth_data => #{password => <<"pencil">>} 205 | } 206 | ). 207 | 208 | configuration_cached_keys_works_easily() -> 209 | [{timetrap, {seconds, 1}}]. 210 | 211 | configuration_cached_keys_works_easily(_Config) -> 212 | Cached = cached_heavy_scram_definitions(), 213 | {ok, ClientState1} = fast_scram:mech_new( 214 | #{ 215 | entity => client, 216 | hash_method => sha, 217 | username => <<"user">>, 218 | cached_challenge => {base64:decode(<<"QSXCR+Q6sek8bf92">>), 409600000}, 219 | auth_data => #{salted_password => Cached#scram_definitions.salted_password} 220 | } 221 | ), 222 | {ok, ServerState2} = fast_scram:mech_new( 223 | #{ 224 | entity => server, 225 | hash_method => sha, 226 | retrieve_mechanism => 227 | fun(_) -> 228 | #{ 229 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 230 | it_count => 409600000, 231 | auth_data => #{ 232 | stored_key => Cached#scram_definitions.stored_key, 233 | server_key => Cached#scram_definitions.server_key 234 | } 235 | } 236 | end 237 | } 238 | ), 239 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 240 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 241 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 242 | {ok, ServerFinal, _} = fast_scram:mech_step(ServerState4, ClientFinal), 243 | {ok, _Final, _ClientState7} = fast_scram:mech_step(ClientState5, ServerFinal). 244 | 245 | configuration_cached_keys_works_easily_v2() -> 246 | [{timetrap, {seconds, 1}}]. 247 | 248 | configuration_cached_keys_works_easily_v2(_Config) -> 249 | Cached = cached_heavy_scram_definitions(), 250 | {ok, ClientState1} = fast_scram:mech_new( 251 | #{ 252 | entity => client, 253 | hash_method => sha, 254 | username => <<"user">>, 255 | cached_challenge => {409600000, base64:decode(<<"QSXCR+Q6sek8bf92">>)}, 256 | auth_data => #{ 257 | client_key => Cached#scram_definitions.client_key, 258 | server_key => Cached#scram_definitions.server_key 259 | } 260 | } 261 | ), 262 | {ok, ServerState2} = fast_scram:mech_new( 263 | #{ 264 | entity => server, 265 | hash_method => sha, 266 | retrieve_mechanism => 267 | fun(_) -> 268 | #{ 269 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 270 | it_count => 409600000, 271 | auth_data => #{ 272 | salted_password => 273 | Cached#scram_definitions.salted_password 274 | } 275 | } 276 | end 277 | } 278 | ), 279 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 280 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 281 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 282 | {ok, ServerFinal, _} = fast_scram:mech_step(ServerState4, ClientFinal), 283 | {ok, _Final, _ClientState7} = fast_scram:mech_step(ClientState5, ServerFinal). 284 | 285 | configuration_cached_wrong_simply_recalculates() -> 286 | [{timetrap, {seconds, 1}}]. 287 | 288 | configuration_cached_wrong_simply_recalculates(_Config) -> 289 | CachedRegular = cached_regular_scram_refinitions(), 290 | CachedHeavy = cached_heavy_scram_definitions(), 291 | {ok, ClientState1} = fast_scram:mech_new( 292 | #{ 293 | entity => client, 294 | hash_method => sha, 295 | username => <<"user">>, 296 | cached_challenge => {409600000, base64:decode(<<"QSXCR+Q6sek8bf92">>)}, 297 | auth_data => #{ 298 | password => <<"pencil">>, 299 | salted_password => CachedHeavy#scram_definitions.salted_password 300 | } 301 | } 302 | ), 303 | {ok, ServerState2} = fast_scram:mech_new( 304 | #{ 305 | entity => server, 306 | hash_method => sha, 307 | retrieve_mechanism => 308 | fun(_) -> 309 | #{ 310 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 311 | it_count => 4096, 312 | auth_data => #{ 313 | password => <<"pencil">>, 314 | stored_key => CachedRegular#scram_definitions.stored_key, 315 | server_key => 316 | CachedRegular#scram_definitions.server_key 317 | } 318 | } 319 | end 320 | } 321 | ), 322 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 323 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 324 | {continue, ClientFinal, ClientState5} = fast_scram:mech_step(ClientState3, ServerFirst), 325 | {ok, ServerFinal, _} = fast_scram:mech_step(ServerState4, ClientFinal), 326 | {ok, _Final, _ClientState7} = fast_scram:mech_step(ClientState5, ServerFinal). 327 | 328 | configuration_cached_wrong_without_password_fails() -> 329 | [{timetrap, {seconds, 1}}]. 330 | 331 | configuration_cached_wrong_without_password_fails(_Config) -> 332 | CachedRegular = cached_regular_scram_refinitions(), 333 | CachedHeavy = cached_heavy_scram_definitions(), 334 | {ok, ClientState1} = fast_scram:mech_new( 335 | #{ 336 | entity => client, 337 | hash_method => sha, 338 | username => <<"user">>, 339 | cached_challenge => {409600000, base64:decode(<<"QSXCR+Q6sek8bf92">>)}, 340 | auth_data => #{ 341 | salted_password => CachedHeavy#scram_definitions.salted_password 342 | } 343 | } 344 | ), 345 | {ok, ServerState2} = fast_scram:mech_new( 346 | #{ 347 | entity => server, 348 | hash_method => sha, 349 | retrieve_mechanism => 350 | fun(_) -> 351 | #{ 352 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 353 | it_count => 4096, 354 | auth_data => #{ 355 | password => <<"pencil">>, 356 | stored_key => CachedRegular#scram_definitions.stored_key, 357 | server_key => 358 | CachedRegular#scram_definitions.server_key 359 | } 360 | } 361 | end 362 | } 363 | ), 364 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 365 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 366 | {continue, ClientFinal, _} = fast_scram:mech_step(ClientState3, ServerFirst), 367 | {error, Reason, _} = fast_scram:mech_step(ServerState4, ClientFinal), 368 | ?assertEqual(<<"e=invalid-proof">>, Reason). 369 | 370 | authentication_server_rejects_the_proof(_Config) -> 371 | ServerState2 = typical_scram_configuration(server), 372 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 373 | WrongProof = 374 | <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,", "p=", 375 | (base64:encode(<<"wrong_proof">>))/binary>>, 376 | {error, Reason, _} = fast_scram:mech_step(ServerState4, WrongProof), 377 | ?assertEqual(<<"e=invalid-proof">>, Reason). 378 | 379 | authentication_server_rejects_invalid_encoded_proof(_Config) -> 380 | ServerState2 = typical_scram_configuration(server), 381 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 382 | WrongProof = <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,", "p=wrong_proof">>, 383 | {error, Reason, _} = fast_scram:mech_step(ServerState4, WrongProof), 384 | ?assertEqual(<<"e=invalid-encoding">>, Reason). 385 | 386 | authentication_client_rejects_the_signature(_Config) -> 387 | ClientState1 = typical_scram_configuration(client), 388 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 389 | {continue, _, ClientState5} = fast_scram:mech_step(ClientState3, server_first()), 390 | WrongSignature = <<"v=", (base64:encode(<<"wrong_signature">>))/binary>>, 391 | {error, Reason, _} = fast_scram:mech_step(ClientState5, WrongSignature), 392 | ?assertEqual(<<"authentication-failure">>, Reason). 393 | 394 | authentication_server_last_message_is_an_error(_Config) -> 395 | ClientState = typical_scram_configuration(client), 396 | {error, Reason, _} = fast_scram:mech_step( 397 | ClientState#fast_scram_state{step = 5}, <<"e=invalid">> 398 | ), 399 | ?assertEqual(<<"invalid">>, Reason). 400 | 401 | configuration_client_sends_wrong_username(_Config) -> 402 | ClientState1 = typical_scram_configuration(client), 403 | ServerState0 = typical_scram_configuration(server), 404 | ServerState2 = ServerState0#fast_scram_state{ 405 | data = #{username => <<"not-user">>, password => <<"pencil">>} 406 | }, 407 | {continue, ClientFirst, _} = fast_scram:mech_step(ClientState1, <<>>), 408 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst), 409 | ?assertEqual(<<"unknown-user">>, Reason). 410 | 411 | verification_name_escapes_values_correctly(_Config) -> 412 | ServerState0 = #fast_scram_state{data = Data} = typical_scram_configuration(server), 413 | ServerState2 = ServerState0#fast_scram_state{ 414 | data = Data#{username => <<"u,ser">>, password => <<"pencil">>} 415 | }, 416 | Username = <<"n,,n=u=2Cser,r=fyko+d2lbbFgONRv9qkxdawL">>, 417 | {NextStep, _, _} = fast_scram:mech_step(ServerState2, Username), 418 | ?assertEqual(continue, NextStep). 419 | 420 | verification_name_does_not_escape_values_correctly(_Config) -> 421 | ServerState0 = typical_scram_configuration(server), 422 | ServerState2 = ServerState0#fast_scram_state{ 423 | data = #{username => <<"u,ser">>, password => <<"pencil">>} 424 | }, 425 | Username = <<"n,,n=u=ser,r=fyko+d2lbbFgONRv9qkxdawL">>, 426 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 427 | ?assertEqual(<<"invalid-username-encoding">>, Reason). 428 | 429 | %% The client MUST verify that the initial part of the nonce used in subsequent messages 430 | %% is the same as the nonce it initially specified 431 | nonce_client_receives_invalid(_Config) -> 432 | ClientState1 = typical_scram_configuration(client), 433 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 434 | ServerWrongNonce = <<"r=clientreceiveswrongnonce3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096">>, 435 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongNonce), 436 | ?assertEqual(<<"invalid-nonce">>, Reason). 437 | 438 | %% The server MUST verify that the nonce sent by the client in the second message 439 | %% is the same as the one sent by the server in its first message. 440 | nonce_server_finds_non_matching(_Config) -> 441 | ServerState2 = typical_scram_configuration(server), 442 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 443 | WrongNonce = 444 | <<"c=biws,r=bad_nonce_FgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=">>, 445 | {error, Reason, _} = fast_scram:mech_step(ServerState4, WrongNonce), 446 | ?assertEqual(<<"e=invalid-nonce">>, Reason). 447 | 448 | %% If the flag is set to "y" and the server supports channel binding, 449 | %% the server MUST fail authentication. 450 | %% This is because if the client sets the channel binding flag to "y", 451 | %% then the client must have believed that the server did not support channel binding 452 | %% -- if the server did in fact support channel binding, 453 | %% then this is an indication that there has been a downgrade attack 454 | %% (e.g., an attacker changed the server’s mechanism list to exclude the -PLUS 455 | %% suffixed SCRAM mechanism name(s)). 456 | channel_binding_client_did_not_see_available_plus(_Config) -> 457 | {ok, ServerState2} = fast_scram:mech_new( 458 | #{ 459 | entity => server, 460 | hash_method => sha, 461 | channel_binding => {<<"tls-unique">>, <<1, 2, 3, 4, 5, 6, 7, 8>>}, 462 | retrieve_mechanism => 463 | fun(_) -> 464 | #{ 465 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 466 | it_count => 4096, 467 | auth_data => #{ 468 | password => <<"pencil">> 469 | } 470 | } 471 | end 472 | } 473 | ), 474 | YesGS2Flag = <<"y,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 475 | {error, Reason, _} = fast_scram:mech_step(ServerState2, YesGS2Flag), 476 | ?assertEqual(<<"server-does-support-channel-binding">>, Reason). 477 | 478 | channel_server_offers_but_client_does_not_take_is_ok(_Config) -> 479 | {ok, ServerState2} = fast_scram:mech_new( 480 | #{ 481 | entity => server, 482 | hash_method => sha, 483 | channel_binding => {<<"tls-unique">>, <<1, 2, 3, 4, 5, 6, 7, 8>>}, 484 | retrieve_mechanism => 485 | fun(_) -> 486 | #{ 487 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 488 | it_count => 4096, 489 | auth_data => #{password => <<"pencil">>} 490 | } 491 | end 492 | } 493 | ), 494 | YesGS2Flag = <<"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 495 | {continue, _, _} = fast_scram:mech_step(ServerState2, YesGS2Flag). 496 | 497 | channel_not_advertise_but_client_could_is_ok(_Config) -> 498 | {ok, ServerState2} = fast_scram:mech_new( 499 | #{ 500 | entity => server, 501 | hash_method => sha, 502 | retrieve_mechanism => 503 | fun(_) -> 504 | #{ 505 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 506 | it_count => 4096, 507 | auth_data => #{password => <<"pencil">>} 508 | } 509 | end 510 | } 511 | ), 512 | YesGS2Flag = <<"y,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 513 | {continue, _, _} = fast_scram:mech_step(ServerState2, YesGS2Flag). 514 | 515 | %% If the channel binding flag was "p" and the server does not support 516 | %% the indicated channel binding type, then the server MUST fail authentication. 517 | channel_type_does_not_match(_Config) -> 518 | {ok, ServerState2} = fast_scram:mech_new( 519 | #{ 520 | entity => server, 521 | hash_method => sha, 522 | channel_binding => {<<"some_server_type">>, <<1, 2, 3, 4, 5, 6, 7, 8>>}, 523 | retrieve_mechanism => 524 | fun(_) -> 525 | #{ 526 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 527 | it_count => 4096, 528 | auth_data => #{password => <<"pencil">>} 529 | } 530 | end 531 | } 532 | ), 533 | ClientFirst = <<"p=some_client_type,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 534 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst), 535 | ?assertEqual(<<"unsupported-channel-binding-type">>, Reason). 536 | 537 | channel_type_matches_but_data_does_not(_Config) -> 538 | {ok, ClientState1} = fast_scram:mech_new( 539 | #{ 540 | entity => client, 541 | hash_method => sha, 542 | username => <<"user">>, 543 | channel_binding => {<<"tls-unique">>, <<1, 2, 3, 4, 5, 6, 7, 8>>}, 544 | auth_data => #{password => <<"pencil">>} 545 | } 546 | ), 547 | {ok, ServerState2} = fast_scram:mech_new( 548 | #{ 549 | entity => server, 550 | hash_method => sha, 551 | channel_binding => {<<"tls-unique">>, <<2, 2, 3, 4, 5, 6, 7, 8>>}, 552 | retrieve_mechanism => 553 | fun(_) -> 554 | #{ 555 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 556 | it_count => 4096, 557 | auth_data => #{password => <<"pencil">>} 558 | } 559 | end 560 | } 561 | ), 562 | {continue, ClientFirst, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 563 | {continue, ServerFirst, ServerState4} = fast_scram:mech_step(ServerState2, ClientFirst), 564 | {continue, ClientFinal, _} = fast_scram:mech_step(ClientState3, ServerFirst), 565 | {error, Reason, _} = fast_scram:mech_step(ServerState4, ClientFinal), 566 | ?assertEqual(<<"e=channel-bindings-dont-match">>, Reason). 567 | 568 | channel_is_not_supported_by_the_server(_Config) -> 569 | ServerState2 = typical_scram_configuration(server), 570 | ClientFirst = <<"p=tls-unique,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 571 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst), 572 | ?assertEqual(<<"channel-binding-not-supported">>, Reason). 573 | 574 | missing_username(_Config) -> 575 | ServerState2 = typical_scram_configuration(server), 576 | Username = <<"n,,,r=fyko+d2lbbFgONRv9qkxdawL">>, 577 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 578 | ?assertEqual(<<"unknown-user">>, Reason). 579 | missing_authzid(_Config) -> 580 | ServerState2 = typical_scram_configuration(server), 581 | Username = <<"n,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 582 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 583 | ?assertEqual(<<"no-resources">>, Reason). 584 | missing_gs2_info(_Config) -> 585 | ServerState2 = typical_scram_configuration(server), 586 | Username = <<",,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 587 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 588 | ?assertEqual(<<"no-resources">>, Reason). 589 | missing_gs2(_Config) -> 590 | ServerState2 = typical_scram_configuration(server), 591 | Username = <<",n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 592 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 593 | ?assertEqual(<<"no-resources">>, Reason). 594 | missing_nonce(_Config) -> 595 | ServerState2 = typical_scram_configuration(server), 596 | Username = <<"n,,n=user,">>, 597 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 598 | ?assertEqual(<<"no-resources">>, Reason). 599 | missing_salt(_Config) -> 600 | ClientState1 = typical_scram_configuration(client), 601 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 602 | ServerWrongNonce = <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,,i=4096">>, 603 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongNonce), 604 | ?assertEqual(<<"no-resources">>, Reason). 605 | missing_it_count(_Config) -> 606 | ClientState1 = typical_scram_configuration(client), 607 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 608 | ServerWrongNonce = <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,">>, 609 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongNonce), 610 | ?assertEqual(<<"no-resources">>, Reason). 611 | missing_proof_info(_Config) -> 612 | ServerState2 = typical_scram_configuration(server), 613 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 614 | WrongProof = <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,">>, 615 | {error, NoResources, _} = fast_scram:mech_step(ServerState4, WrongProof), 616 | ?assertEqual(<<"e=other-error">>, NoResources), 617 | EmptyProof = <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=">>, 618 | {error, InvalidProof, _} = fast_scram:mech_step(ServerState4, EmptyProof), 619 | ?assertEqual(<<"e=invalid-proof">>, InvalidProof). 620 | missing_proof(_Config) -> 621 | ServerState2 = typical_scram_configuration(server), 622 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 623 | WrongProof = <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j">>, 624 | {error, Reason, _} = fast_scram:mech_step(ServerState4, WrongProof), 625 | ?assertEqual(<<"e=other-error">>, Reason). 626 | missing_channel_binding_info(_Config) -> 627 | ServerState2 = typical_scram_configuration(server), 628 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 629 | MissingCB = <<",r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=">>, 630 | {error, Reason, _} = fast_scram:mech_step(ServerState4, MissingCB), 631 | ?assertEqual(<<"e=no-resources">>, Reason). 632 | missing_channel_binding(_Config) -> 633 | ServerState2 = typical_scram_configuration(server), 634 | {continue, _, ServerState4} = fast_scram:mech_step(ServerState2, client_first()), 635 | MissingCB = <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=">>, 636 | {error, Reason, _} = fast_scram:mech_step(ServerState4, MissingCB), 637 | ?assertEqual(<<"e=no-resources">>, Reason). 638 | 639 | wrong_flag_username(_Config) -> 640 | ServerState2 = typical_scram_configuration(server), 641 | Username = <<"n,,wrong,r=fyko+d2lbbFgONRv9qkxdawL">>, 642 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 643 | ?assertEqual(<<"other-error">>, Reason). 644 | wrong_flag_g2s(_Config) -> 645 | ServerState2 = typical_scram_configuration(server), 646 | Username = <<"wrong,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 647 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 648 | ?assertEqual(<<"other-error">>, Reason). 649 | wrong_flag_nonce(_Config) -> 650 | ServerState2 = typical_scram_configuration(server), 651 | Username = <<"n,,n=user,wrong">>, 652 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 653 | ?assertEqual(<<"other-error">>, Reason). 654 | wrong_flag_salt(_Config) -> 655 | ClientState1 = typical_scram_configuration(client), 656 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 657 | ServerWrongNonce = <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,wrong,i=4096">>, 658 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongNonce), 659 | ?assertEqual(<<"other-error">>, Reason). 660 | wrong_flag_it_count(_Config) -> 661 | ClientState1 = typical_scram_configuration(client), 662 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 663 | ServerWrongItCount = 664 | <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,wrong">>, 665 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongItCount), 666 | ?assertEqual(<<"other-error">>, Reason). 667 | wrong_it_count(_Config) -> 668 | ClientState1 = typical_scram_configuration(client), 669 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 670 | ServerWrongItCount = 671 | <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=wrong">>, 672 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWrongItCount), 673 | ?assertEqual(<<"invalid-iteration-count">>, Reason). 674 | too_much_input(_Config) -> 675 | ServerState2 = typical_scram_configuration(server), 676 | Username = <<"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=toomuch">>, 677 | {error, Reason, _} = fast_scram:mech_step(ServerState2, Username), 678 | ?assertEqual(<<"error-too-much-input">>, Reason). 679 | 680 | not_supported_authzid(_Config) -> 681 | ServerState2 = typical_scram_configuration(server), 682 | ClientFirst = <<"n,a=other_user,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>, 683 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst), 684 | ?assertEqual(<<"authzid-flag-not-supported">>, Reason). 685 | not_supported_mext(_Config) -> 686 | ClientState1 = typical_scram_configuration(client), 687 | {continue, _, ClientState3} = fast_scram:mech_step(ClientState1, <<>>), 688 | ServerWithMext = 689 | <<"m=mext,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096">>, 690 | {error, Reason, _} = fast_scram:mech_step(ClientState3, ServerWithMext), 691 | ?assertEqual(<<"extensions-not-supported">>, Reason). 692 | not_supported_extension(_Config) -> 693 | ServerState2 = typical_scram_configuration(server), 694 | ClientFirst1 = <<"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL,t=extension">>, 695 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst1), 696 | ?assertEqual(<<"extensions-not-supported">>, Reason), 697 | ClientFirst2 = <<"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL,m=extension">>, 698 | {error, Reason, _} = fast_scram:mech_step(ServerState2, ClientFirst2), 699 | ?assertEqual(<<"extensions-not-supported">>, Reason). 700 | 701 | %%%=================================================================== 702 | %%% Helper functions 703 | %%%=================================================================== 704 | 705 | typical_scram_configuration(Entity) -> 706 | typical_scram_configuration(Entity, #{password => <<"pencil">>}, #{}). 707 | 708 | typical_scram_configuration(client, AuthData, Other) -> 709 | Config0 = #{ 710 | entity => client, 711 | hash_method => sha, 712 | username => <<"user">>, 713 | nonce => <<"fyko+d2lbbFgONRv9qkxdawL">>, 714 | auth_data => AuthData 715 | }, 716 | Config1 = maps:merge(Config0, Other), 717 | {ok, St} = fast_scram:mech_new(Config1), 718 | St; 719 | typical_scram_configuration(server, AuthData, Other) -> 720 | Config0 = #{ 721 | entity => server, 722 | hash_method => sha, 723 | nonce => <<"3rfcNHYJY1ZVvWVs7j">>, 724 | retrieve_mechanism => fun(U, S) -> retrieve_mechanism(U, AuthData, S) end 725 | }, 726 | Config1 = maps:merge(Config0, Other), 727 | {ok, St} = fast_scram:mech_new(Config1), 728 | St. 729 | 730 | retrieve_mechanism(_, AuthData, S) -> 731 | X = #{ 732 | salt => base64:decode(<<"QSXCR+Q6sek8bf92">>), 733 | it_count => 4096, 734 | auth_data => AuthData 735 | }, 736 | {X, S}. 737 | 738 | client_first() -> <<"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL">>. 739 | server_first() -> <<"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096">>. 740 | client_final() -> 741 | <<"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=">>. 742 | server_final() -> <<"v=rmF9pqV8S7suAoZWja4dJRkFsKQ=">>. 743 | 744 | %%% Precalculated with an iteration cout of 4096 745 | cached_regular_scram_refinitions() -> 746 | #scram_definitions{ 747 | hash_method = sha, 748 | salted_password = 749 | <<29, 150, 238, 58, 82, 155, 90, 95, 158, 71, 192, 31, 34, 154, 44, 184, 166, 225, 95, 750 | 125>>, 751 | client_key = 752 | <<226, 52, 196, 123, 246, 195, 102, 150, 221, 109, 133, 43, 153, 170, 162, 186, 38, 85, 753 | 87, 40>>, 754 | stored_key = 755 | <<233, 217, 70, 96, 195, 157, 101, 195, 143, 186, 217, 28, 53, 143, 20, 218, 14, 239, 756 | 43, 214>>, 757 | auth_message = 758 | <<"n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j">>, 759 | client_signature = 760 | <<93, 113, 56, 196, 134, 176, 191, 171, 223, 73, 227, 226, 218, 139, 214, 229, 199, 157, 761 | 182, 19>>, 762 | client_proof = 763 | <<191, 69, 252, 191, 112, 115, 217, 61, 2, 36, 102, 201, 67, 33, 116, 95, 225, 200, 225, 764 | 59>>, 765 | server_key = 766 | <<15, 224, 146, 88, 179, 172, 133, 43, 165, 2, 204, 98, 186, 144, 62, 170, 205, 191, 767 | 125, 49>>, 768 | server_signature = 769 | <<174, 97, 125, 166, 165, 124, 75, 187, 46, 2, 134, 86, 141, 174, 29, 37, 25, 5, 176, 770 | 164>> 771 | }. 772 | 773 | %%% This was calculated for the same parameters than as above, 774 | %%% except that the iteration count is 409600000, so it would take a long time 775 | cached_heavy_scram_definitions() -> 776 | #scram_definitions{ 777 | hash_method = sha, 778 | salted_password = 779 | <<88, 214, 221, 58, 163, 214, 103, 20, 235, 222, 209, 209, 41, 158, 166, 159, 61, 23, 780 | 116, 62>>, 781 | client_key = 782 | <<30, 240, 57, 94, 35, 56, 109, 230, 129, 154, 73, 94, 142, 182, 50, 156, 78, 128, 171, 783 | 29>>, 784 | stored_key = 785 | <<31, 45, 16, 146, 4, 98, 92, 24, 40, 167, 241, 234, 95, 61, 79, 194, 127, 194, 197, 786 | 103>>, 787 | auth_message = 788 | <<"n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=409600000,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j">>, 789 | client_signature = 790 | <<209, 237, 202, 96, 155, 147, 96, 108, 48, 218, 110, 140, 122, 223, 86, 215, 92, 171, 791 | 193, 19>>, 792 | client_proof = 793 | <<207, 29, 243, 62, 184, 171, 13, 138, 177, 64, 39, 210, 244, 105, 100, 75, 18, 43, 106, 794 | 14>>, 795 | server_key = 796 | <<222, 94, 85, 104, 169, 47, 131, 128, 114, 6, 162, 90, 225, 108, 19, 88, 133, 210, 62, 797 | 187>>, 798 | server_signature = 799 | <<87, 156, 101, 76, 203, 139, 198, 161, 160, 20, 24, 9, 165, 245, 125, 35, 171, 138, 16, 800 | 195>> 801 | }. 802 | --------------------------------------------------------------------------------