├── .editorconfig ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── srp.ex └── srp │ ├── client.ex │ ├── group.ex │ ├── identity.ex │ ├── identity_verifier.ex │ ├── key_pair.ex │ ├── math.ex │ └── server.ex ├── mix.exs ├── mix.lock └── test ├── srp ├── client_test.exs ├── identity_test.exs ├── identity_verifier_test.exs └── server_test.exs ├── srp_test.exs └── test_helper.exs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | /docs/ 13 | 14 | # Ignore .fetch files in case you like to edit your project deps locally. 15 | /.fetch 16 | 17 | # If the VM crashes, it generates a dump, let's ignore it too. 18 | erl_crash.dump 19 | 20 | # Also ignore archive artifacts (built via "mix archive.build"). 21 | *.ez 22 | 23 | # Ignore package tarball (built via "mix hex.build"). 24 | srp-*.tar 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | cache: 4 | directories: 5 | - ~/.hex 6 | - ~/.mix 7 | - deps 8 | 9 | elixir: 10 | - 1.6.6 11 | - 1.7.2 12 | 13 | otp_release: 14 | - 20.3 15 | - 21.0 16 | 17 | script: 18 | - mix format --check-formatted 19 | - mix credo --strict 20 | - mix test --exclude property 21 | - mix coveralls.travis 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.0] - 2018-11-05 10 | ### Added 11 | - Option to change random size. 12 | 13 | ### Changed 14 | - Change default random size to 32 bytes (256 bits). 15 | - Leftpad parameters. 16 | 17 | ## [0.1.1] - 2018-11-03 18 | ### Added 19 | - Mix metadata. 20 | 21 | ## [0.1.0] - 2018-11-03 22 | ### Added 23 | - Documentation. 24 | - This CHANGELOG file. 25 | - Property based tests powered by [stream_data]. 26 | - `SRP.Client` behaviour. 27 | - `SRP.Server` behaviour. 28 | - Function `generate_verifier/2`. 29 | - Function `server_key_pair/2`. 30 | - Function `client_key_pair/1`. 31 | - Function `client_proof/5`. 32 | - Function `valid_client_proof?/5`. 33 | - Function `server_proof/5`. 34 | - Function `valid_server_proof?/6`. 35 | - Support all prime group from [RFC 5054]. 36 | 37 | [Unreleased]: https://github.com/thiamsantos/srp-elixir/compare/v0.2.0...HEAD 38 | [0.1.0]: https://github.com/thiamsantos/srp-elixir/tree/v0.1.0 39 | [0.1.1]: https://github.com/thiamsantos/srp-elixir/tree/v0.1.1 40 | [0.2.0]: https://github.com/thiamsantos/srp-elixir/tree/v0.2.0 41 | [stream_data]: https://hex.pm/packages/stream_data 42 | [RFC 5054]: https://tools.ietf.org/html/rfc5054 43 | -------------------------------------------------------------------------------- /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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Srp 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/srp.svg)](https://hex.pm/packages/srp) 4 | [![Docs](https://img.shields.io/badge/hex-docs-green.svg)](https://hexdocs.pm/srp) 5 | [![Build Status](https://travis-ci.com/thiamsantos/srp-elixir.svg?branch=master)](https://travis-ci.com/thiamsantos/srp-elixir) 6 | [![Coverage Status](https://coveralls.io/repos/github/thiamsantos/srp-elixir/badge.svg?branch=master)](https://coveralls.io/github/thiamsantos/srp-elixir?branch=master) 7 | 8 | > Secure Remote Password Protocol implementation in elixir. 9 | 10 | SRP provides an implementation of the Secure Remote Password Protocol presented on 11 | [The SRP Authentication and Key Exchange System](https://tools.ietf.org/html/rfc2945), 12 | [Using the Secure Remote Password (SRP) Protocol for TLS Authentication](https://tools.ietf.org/html/rfc5054) 13 | and [The Secure Remote Password Protocol](http://srp.stanford.edu/ndss.html). 14 | 15 | The protocol provides a way to do zero-knowledge authentication between client and servers. 16 | By using the SRP protocol you can: 17 | - authenticate without ever sending a password over the network. 18 | - authenticate without the risk of anyone learning any of your secrets – even 19 | if they intercept your communication. 20 | - authenticate both the identity of the client and the server to guarantee 21 | that a client isn’t communicating with an impostor server. 22 | 23 | ## Installation 24 | 25 | The package can be installed by adding `srp` to your list of dependencies in `mix.exs`: 26 | 27 | ```elixir 28 | def deps do 29 | [ 30 | {:srp, "~> 0.2.0"} 31 | ] 32 | end 33 | ``` 34 | 35 | ## Usage 36 | 37 | Checkout the full [documentation](https://hexdocs.pm/srp) for a complete usage. 38 | 39 | ### Signing up 40 | 41 | After the user provides his username and password, the client must generate 42 | a password verifier. Then it must send to the server: 43 | 44 | - The username for future identification. 45 | - The password verifier that will be used in the future to verify the client credentials. 46 | - The salt used in the process. 47 | 48 | ```elixir 49 | username = "alice" 50 | password = "password123" 51 | 52 | identity = SRP.new_identity(username, password) 53 | %SRP.IdentityVerifier{username: username, salt: salt, password_verifier: password_verifier} = 54 | SRP.generate_verifier(identity) 55 | 56 | # Send to the server -> username + salt + password_verifier 57 | # Server stores the information 58 | ``` 59 | 60 | ### Logging in 61 | 62 | Authenticating a user on the server involves multiple steps. 63 | 64 | 1. The client sends to the server the username. 65 | 2. The server finds the password verifier and salt for that username. 66 | Then it generates a ephemeral key pair and sends back to the client the salt and the public key. 67 | 68 | ```elixir 69 | # Find the record for the given username 70 | # Load from the database the password_verifier, and the salt 71 | key_pair = SRP.server_key_pair(password_verifier) 72 | 73 | # Send back to the client -> key_pair.public + salt 74 | ``` 75 | 76 | If the username does not exist the server can send a fake value. 77 | It is important to not reveal if an username is registered on the system or not. 78 | An attacker could use the login to find the registered usernames 79 | and try a dictionary attack specific for those users. 80 | 81 | 3. The client generates a key pair and a client proof of identity. 82 | Then the client sends to the server the proof and the client's public key. 83 | 84 | ```elixir 85 | # receives from the server the server_public_key and the salt. 86 | 87 | identity = SRP.new_identity(username, password) 88 | key_pair = SRP.client_key_pair() 89 | proof = SRP.client_proof(identity, salt, key_pair, server_public_key) 90 | 91 | # Send to the server -> proof + server_public_key 92 | ``` 93 | 94 | 4. Server verify client proof then build its own proof of identity. 95 | Then sends back the server's proof. 96 | 97 | ```elixir 98 | valid? = SRP.valid_client_proof?(client_proof, password_verifier, server_key_wpair, client_public_key) 99 | 100 | if valid? do 101 | # Send back to client the server's proof -> server_proof 102 | else 103 | # Send back unauthorized 104 | end 105 | ``` 106 | 107 | 5. The client receives the server's proof and validates it. 108 | This step can be skipped if you don't feel the need to verify the server's identity. 109 | 110 | ```elixir 111 | identity = SRP.new_identity(username, password) 112 | valid? = SRP.valid_server_proof?(server_proof, identity, salt, client_key_pair, server_public_key) 113 | ``` 114 | 115 | From now on is to safe to create a new session between the client and server. 116 | 117 | ## License 118 | 119 | [Apache License, Version 2.0](LICENSE.md) © [Thiago Santos](https://github.com/thiamsantos) 120 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :srp, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:srp, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/srp.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP do 2 | @moduledoc """ 3 | SRP provides an implementation of the Secure Remote Password Protocol presented on 4 | [The SRP Authentication and Key Exchange System](https://tools.ietf.org/html/rfc2945), 5 | [Using the Secure Remote Password (SRP) Protocol for TLS Authentication](https://tools.ietf.org/html/rfc5054) 6 | and [The Secure Remote Password Protocol](http://srp.stanford.edu/ndss.html). 7 | 8 | The protocol provides a way to do zero-knowledge authentication between client and servers. 9 | By using the SRP protocol you can: 10 | - authenticate without ever sending a password over the network. 11 | - authenticate without the risk of anyone learning any of your secrets – even 12 | if they intercept your communication. 13 | - authenticate both the identity of the client and the server to guarantee 14 | that a client isn’t communicating with an impostor server. 15 | 16 | ## Signing up 17 | 18 | After the user provides his username and password, the client must generate 19 | a password verifier. Then it must send to the server: 20 | 21 | - The username for future identification. 22 | - The password verifier that will be used in the future to verify the client credentials. 23 | - The salt used in the process. 24 | 25 | ```elixir 26 | username = "alice" 27 | password = "password123" 28 | 29 | identity = SRP.new_identity(username, password) 30 | %SRP.IdentityVerifier{username: username, salt: salt, password_verifier: password_verifier} = 31 | SRP.generate_verifier(identity) 32 | 33 | # Send to the server -> username + salt + password_verifier 34 | # Server stores the information 35 | ``` 36 | 37 | ## Logging in 38 | 39 | Authenticating a user on the server involves multiple steps. 40 | 41 | 1. The client sends to the server the username. 42 | 2. The server finds the password verifier and salt for that username. 43 | Then it generates a ephemeral key pair and sends back to the client the salt and the public key. 44 | 45 | ```elixir 46 | # Find the record for the given username 47 | # Load from the database the password_verifier, and the salt 48 | key_pair = SRP.server_key_pair(password_verifier) 49 | 50 | # Send back to the client -> key_pair.public + salt 51 | ``` 52 | 53 | If the username does not exist the server can send a fake value. 54 | It is important to not reveal if an username is registered on the system or not. 55 | An attacker could use the login to find the registered usernames 56 | and try a dictionary attack specific for those users. 57 | 58 | 3. The client generates a key pair and a client proof of identity. 59 | Then the client sends to the server the proof and the client's public key. 60 | 61 | ```elixir 62 | # receives from the server the server_public_key and the salt. 63 | 64 | identity = SRP.new_identity(username, password) 65 | key_pair = SRP.client_key_pair() 66 | proof = SRP.client_proof(identity, salt, key_pair, server_public_key) 67 | 68 | # Send to the server -> proof + server_public_key 69 | ``` 70 | 71 | 4. Server verify client proof then build its own proof of identity. 72 | Then sends back the server's proof. 73 | 74 | ```elixir 75 | valid? = SRP.valid_client_proof?(client_proof, password_verifier, server_key_pair, client_public_key) 76 | 77 | if valid? do 78 | # Send back to client the server's proof -> server_proof 79 | else 80 | # Send back unauthorized 81 | end 82 | ``` 83 | 84 | 5. The client receives the server's proof and validates it. 85 | This step can be skipped if you don't feel the need to verify the server's identity. 86 | 87 | ```elixir 88 | identity = SRP.new_identity(username, password) 89 | valid? = SRP.valid_server_proof?(server_proof, identity, salt, client_key_pair, server_public_key) 90 | ``` 91 | 92 | From now on is to safe to create a new session between the client and server. 93 | 94 | ## Prime Groups 95 | 96 | The default prime size is 2048. Each prime group contains a large prime and a generator. 97 | These two values are used to derive several values on the calculations defined by the RFC. 98 | 99 | The 1024-, 1536-, and 2048-bit groups are taken from software developed by Tom 100 | Wu and Eugene Jhong for the Stanford SRP distribution, and subsequently proven 101 | to be prime. The larger primes are taken from 102 | [More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE)](https://tools.ietf.org/html/rfc3526), 103 | but generators have been calculated that are primitive roots of N, unlike the generators in 104 | [More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE)](https://tools.ietf.org/html/rfc3526). 105 | 106 | The following prime sizes are supported by SRP: 107 | 108 | - 1024 109 | - 1536 110 | - 2048 111 | - 3072 112 | - 4096 113 | - 6144 114 | - 8192 115 | 116 | ## Hash Algorithm 117 | 118 | By default the algorithm is SHA-1 because it is the algorithm used on the RFC. 119 | The SRP protocol uses a hash function to derive several values: 120 | 121 | - The hash of the public keys prevents an attacker who learns a user's verifier 122 | from being able to authenticate as that user. 123 | - The hash of the prime group prevents an attacker who can select group parameters 124 | from being able to launch a 2-for-1 guessing attack. 125 | - Another hash contains the user's password mixed with a salt. 126 | 127 | Cryptanalytic attacks against SHA-1 that only affect its collision- 128 | resistance do not compromise these uses. If attacks against SHA-1 129 | are discovered that do compromise these uses, new cipher suites 130 | should be specified to use a different hash algorithm. 131 | 132 | The following hash algorithms are supported by SRP: 133 | 134 | - sha 135 | - sha224 136 | - sha256 137 | - sha384 138 | - sha512 139 | - md4 140 | - md5 141 | 142 | ## Shared options 143 | 144 | Almost all of the srp function below accept the following options: 145 | 146 | - `:prime_size` - The size of the prime to be used on the calculations (default: `2048`); 147 | - `:hash_algorithm` - The hash algorithm used to derive several values (default: `:sha`); 148 | - `:random_bytes` - Quantity of random bytes used internally (default: `32`) 149 | 150 | """ 151 | 152 | import SRP.Math 153 | alias SRP.{Group, Identity, IdentityVerifier, KeyPair} 154 | require SRP.Group 155 | 156 | @default_options [prime_size: 2048, hash_algorithm: :sha, random_bytes: 32] 157 | 158 | @doc """ 159 | Generate a identity verifier that should be passed to the server during account creation. 160 | 161 | ## Examples 162 | 163 | iex> alice_identity = SRP.new_identity("alice", "password123") 164 | iex> %SRP.IdentityVerifier{username: "alice", salt: salt, password_verifier: password_verifier} = 165 | ...> SRP.generate_verifier(alice_identity) 166 | iex> is_binary(salt) 167 | true 168 | iex> is_binary(password_verifier) 169 | true 170 | 171 | iex> bob_identity = SRP.new_identity("bob", "password123") 172 | iex> %SRP.IdentityVerifier{username: "bob", salt: salt, password_verifier: password_verifier} = 173 | ...> SRP.generate_verifier(bob_identity, hash_algorithm: :sha512) 174 | iex> is_binary(salt) 175 | true 176 | iex> is_binary(password_verifier) 177 | true 178 | 179 | iex> kirk_identity = SRP.new_identity("kirk", "password123") 180 | iex> %SRP.IdentityVerifier{username: "kirk", salt: salt, password_verifier: password_verifier} = 181 | ...> SRP.generate_verifier(kirk_identity, prime_size: 1024) 182 | iex> is_binary(salt) 183 | true 184 | iex> is_binary(password_verifier) 185 | true 186 | 187 | iex> spock_identity = SRP.new_identity("spock", "password123") 188 | iex> %SRP.IdentityVerifier{username: "spock", salt: salt, password_verifier: password_verifier} = 189 | ...> SRP.generate_verifier(spock_identity, prime_size: 8192, hash_algorithm: :sha256) 190 | iex> is_binary(salt) 191 | true 192 | iex> is_binary(password_verifier) 193 | true 194 | 195 | """ 196 | @spec generate_verifier(Identity.t(), Keyword.t()) :: IdentityVerifier.t() 197 | def generate_verifier(%Identity{username: username, password: password}, options \\ []) do 198 | options = Keyword.merge(@default_options, options) 199 | prime_size = Keyword.get(options, :prime_size) 200 | hash_algorithm = Keyword.get(options, :hash_algorithm) 201 | random_bytes = Keyword.get(options, :random_bytes) 202 | 203 | %Group{prime: prime, generator: generator} = Group.get(prime_size) 204 | 205 | salt = random(random_bytes) 206 | credentials = hash(hash_algorithm, salt <> hash(hash_algorithm, username <> ":" <> password)) 207 | password_verifier = mod_pow(generator, credentials, prime) 208 | 209 | IdentityVerifier.new(username, salt, password_verifier) 210 | end 211 | 212 | @doc """ 213 | Create a new `SRP.Identity` struct. 214 | 215 | ## Examples 216 | 217 | iex> SRP.new_identity("alice", "password123") 218 | %SRP.Identity{username: "alice", password: "password123"} 219 | 220 | """ 221 | def new_identity(username, password) when is_binary(username) and is_binary(password) do 222 | %Identity{ 223 | username: username, 224 | password: password 225 | } 226 | end 227 | 228 | @doc """ 229 | Generate a ephemeral key pair for the server. 230 | The private key is randomly generated, and the public key is 231 | derived from the private key and the password verifier. 232 | 233 | ## Examples 234 | 235 | iex> password_verifier = Base.decode16!("BEB25379D1A8581EB5A727673A2441EE") 236 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 237 | ...> SRP.server_key_pair(password_verifier) 238 | iex> is_binary(public_key) 239 | true 240 | iex> is_binary(private_key) 241 | true 242 | 243 | iex> password_verifier = Base.decode16!("BEB25379D1A8581EB5A727673A2441EE") 244 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 245 | ...> SRP.server_key_pair(password_verifier, hash_algorithm: :sha512) 246 | iex> is_binary(public_key) 247 | true 248 | iex> is_binary(private_key) 249 | true 250 | 251 | iex> password_verifier = Base.decode16!("BEB25379D1A8581EB5A727673A2441EE") 252 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 253 | ...> SRP.server_key_pair(password_verifier, prime_size: 1024) 254 | iex> is_binary(public_key) 255 | true 256 | iex> is_binary(private_key) 257 | true 258 | 259 | iex> password_verifier = Base.decode16!("BEB25379D1A8581EB5A727673A2441EE") 260 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 261 | ...> SRP.server_key_pair(password_verifier, prime_size: 8192, hash_algorithm: :sha256) 262 | iex> is_binary(public_key) 263 | true 264 | iex> is_binary(private_key) 265 | true 266 | 267 | """ 268 | @spec server_key_pair(binary(), Keyword.t()) :: KeyPair.t() 269 | def server_key_pair(password_verifier, options \\ []) when is_binary(password_verifier) do 270 | options = Keyword.merge(@default_options, options) 271 | prime_size = Keyword.get(options, :prime_size) 272 | hash_algorithm = Keyword.get(options, :hash_algorithm) 273 | random_bytes = Keyword.get(options, :random_bytes) 274 | 275 | %Group{prime: prime, prime_length: prime_length, generator: generator} = Group.get(prime_size) 276 | 277 | multiplier = hash(hash_algorithm, prime <> String.pad_leading(generator, prime_length)) 278 | private_key = random(random_bytes) 279 | 280 | public_key = 281 | add( 282 | mult(multiplier, password_verifier), 283 | mod_pow(generator, private_key, prime) 284 | ) 285 | 286 | %KeyPair{private: private_key, public: :binary.encode_unsigned(public_key)} 287 | end 288 | 289 | @doc """ 290 | Generate a ephemeral key pair for the client. 291 | The private key is randomly generated and 292 | then the public key is derived from the private key. 293 | 294 | ## Examples 295 | 296 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 297 | ...> SRP.client_key_pair() 298 | iex> is_binary(public_key) 299 | true 300 | iex> is_binary(private_key) 301 | true 302 | 303 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 304 | ...> SRP.client_key_pair(hash_algorithm: :sha512) 305 | iex> is_binary(public_key) 306 | true 307 | iex> is_binary(private_key) 308 | true 309 | 310 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 311 | ...> SRP.client_key_pair(prime_size: 1024) 312 | iex> is_binary(public_key) 313 | true 314 | iex> is_binary(private_key) 315 | true 316 | 317 | iex> %SRP.KeyPair{public: public_key, private: private_key} = 318 | ...> SRP.client_key_pair(prime_size: 8192, hash_algorithm: :sha256) 319 | iex> is_binary(public_key) 320 | true 321 | iex> is_binary(private_key) 322 | true 323 | 324 | """ 325 | @spec client_key_pair(Keyword.t()) :: KeyPair.t() 326 | def client_key_pair(options \\ []) do 327 | options = Keyword.merge(@default_options, options) 328 | prime_size = Keyword.get(options, :prime_size) 329 | random_bytes = Keyword.get(options, :random_bytes) 330 | 331 | %Group{prime: prime, generator: generator} = Group.get(prime_size) 332 | 333 | private_key = random(random_bytes) 334 | public_key = mod_pow(generator, private_key, prime) 335 | 336 | %KeyPair{private: private_key, public: public_key} 337 | end 338 | 339 | @doc """ 340 | Generate a proof of identity for the client. 341 | 342 | ## Examples 343 | 344 | iex> identity = SRP.new_identity("bob", "password123") 345 | iex> identity_verifier = SRP.generate_verifier(identity) 346 | iex> client_key_pair = SRP.client_key_pair() 347 | iex> server_key_pair = SRP.server_key_pair(identity_verifier.password_verifier) 348 | iex> proof = 349 | ...> SRP.client_proof( 350 | ...> identity, 351 | ...> identity_verifier.salt, 352 | ...> client_key_pair, 353 | ...> server_key_pair.public 354 | ...> ) 355 | iex> is_binary(proof) 356 | true 357 | 358 | """ 359 | @spec client_proof(binary(), binary(), KeyPair.t(), binary(), Keyword.t()) :: binary() 360 | def client_proof( 361 | %Identity{} = identity, 362 | salt, 363 | %KeyPair{} = client_key_pair, 364 | server_public_key, 365 | options \\ [] 366 | ) do 367 | premaster_secret = 368 | generate_client_premaster_secret( 369 | identity, 370 | salt, 371 | client_key_pair, 372 | server_public_key, 373 | options 374 | ) 375 | 376 | generate_client_proof(client_key_pair.public, server_public_key, premaster_secret, options) 377 | end 378 | 379 | @doc """ 380 | Validate the client's proof of identity. 381 | 382 | ## Examples 383 | 384 | iex> identity = SRP.new_identity("bob", "password123") 385 | iex> identity_verifier = SRP.generate_verifier(identity) 386 | iex> client_key_pair = SRP.client_key_pair() 387 | iex> server_key_pair = SRP.server_key_pair(identity_verifier.password_verifier) 388 | iex> client_proof = 389 | ...> SRP.client_proof( 390 | ...> identity, 391 | ...> identity_verifier.salt, 392 | ...> client_key_pair, 393 | ...> server_key_pair.public 394 | ...> ) 395 | iex> SRP.valid_client_proof?( 396 | ...> client_proof, 397 | ...> identity_verifier.password_verifier, 398 | ...> server_key_pair, 399 | ...> client_key_pair.public 400 | ...> ) 401 | true 402 | 403 | """ 404 | @spec valid_client_proof?(binary(), binary(), KeyPair.t(), binary(), Keyword.t()) :: boolean() 405 | def valid_client_proof?( 406 | client_proof, 407 | password_verifier, 408 | %KeyPair{} = server_key_pair, 409 | client_public_key, 410 | options \\ [] 411 | ) do 412 | premaster_secret = 413 | generate_server_premaster_secret( 414 | password_verifier, 415 | server_key_pair, 416 | client_public_key, 417 | options 418 | ) 419 | 420 | client_proof == 421 | generate_client_proof(client_public_key, server_key_pair.public, premaster_secret, options) 422 | end 423 | 424 | @doc """ 425 | Generate a proof of identity for the server. 426 | 427 | ## Examples 428 | 429 | iex> identity = SRP.new_identity("bob", "password123") 430 | iex> identity_verifier = SRP.generate_verifier(identity) 431 | iex> client_key_pair = SRP.client_key_pair() 432 | iex> server_key_pair = SRP.server_key_pair(identity_verifier.password_verifier) 433 | iex> client_proof = 434 | ...> SRP.client_proof( 435 | ...> identity, 436 | ...> identity_verifier.salt, 437 | ...> client_key_pair, 438 | ...> server_key_pair.public 439 | ...> ) 440 | iex> server_proof = 441 | ...> SRP.server_proof( 442 | ...> client_proof, 443 | ...> identity_verifier.password_verifier, 444 | ...> server_key_pair, 445 | ...> client_key_pair.public 446 | ...> ) 447 | iex> is_binary(server_proof) 448 | true 449 | 450 | """ 451 | @spec server_proof(binary(), binary(), KeyPair.t(), binary(), Keyword.t()) :: binary() 452 | def server_proof( 453 | client_proof, 454 | password_verifier, 455 | %KeyPair{} = server_key_pair, 456 | client_public_key, 457 | options \\ [] 458 | ) do 459 | premaster_secret = 460 | generate_server_premaster_secret( 461 | password_verifier, 462 | server_key_pair, 463 | client_public_key, 464 | options 465 | ) 466 | 467 | generate_server_proof(client_proof, client_public_key, premaster_secret, options) 468 | end 469 | 470 | @doc """ 471 | Validate the server's proof of identity. 472 | 473 | ## Examples 474 | 475 | iex> identity = SRP.new_identity("bob", "password123") 476 | iex> identity_verifier = SRP.generate_verifier(identity) 477 | iex> client_key_pair = SRP.client_key_pair() 478 | iex> server_key_pair = SRP.server_key_pair(identity_verifier.password_verifier) 479 | iex> client_proof = 480 | ...> SRP.client_proof( 481 | ...> identity, 482 | ...> identity_verifier.salt, 483 | ...> client_key_pair, 484 | ...> server_key_pair.public 485 | ...> ) 486 | iex> server_proof = 487 | ...> SRP.server_proof( 488 | ...> client_proof, 489 | ...> identity_verifier.password_verifier, 490 | ...> server_key_pair, 491 | ...> client_key_pair.public 492 | ...> ) 493 | iex> SRP.valid_server_proof?( 494 | ...> server_proof, 495 | ...> identity, 496 | ...> identity_verifier.salt, 497 | ...> client_key_pair, 498 | ...> server_key_pair.public 499 | ...> ) 500 | true 501 | 502 | """ 503 | @spec valid_server_proof?(binary(), Identity.t(), binary(), KeyPair.t(), binary(), Keyword.t()) :: 504 | boolean() 505 | def valid_server_proof?( 506 | server_proof, 507 | %Identity{} = identity, 508 | salt, 509 | %KeyPair{} = client_key_pair, 510 | server_public_key, 511 | options \\ [] 512 | ) do 513 | premaster_secret = 514 | generate_client_premaster_secret( 515 | identity, 516 | salt, 517 | client_key_pair, 518 | server_public_key, 519 | options 520 | ) 521 | 522 | client_proof = 523 | generate_client_proof(client_key_pair.public, server_public_key, premaster_secret, options) 524 | 525 | server_proof == 526 | generate_server_proof(client_proof, client_key_pair.public, premaster_secret, options) 527 | end 528 | 529 | defp generate_client_premaster_secret( 530 | %Identity{username: username, password: password}, 531 | salt, 532 | %KeyPair{} = client_key_pair, 533 | server_public_key, 534 | options 535 | ) 536 | when is_binary(salt) and is_binary(server_public_key) do 537 | options = Keyword.merge(@default_options, options) 538 | prime_size = Keyword.get(options, :prime_size) 539 | hash_algorithm = Keyword.get(options, :hash_algorithm) 540 | 541 | %Group{prime: prime, prime_length: prime_length, generator: generator} = Group.get(prime_size) 542 | 543 | scrambling = 544 | hash( 545 | hash_algorithm, 546 | String.pad_leading(client_key_pair.public, prime_length) <> 547 | String.pad_leading(server_public_key, prime_length) 548 | ) 549 | 550 | multiplier = hash(hash_algorithm, prime <> String.pad_leading(generator, prime_length)) 551 | credentials = hash(hash_algorithm, salt <> hash(hash_algorithm, username <> ":" <> password)) 552 | 553 | mod_pow( 554 | sub(server_public_key, mult(multiplier, mod_pow(generator, credentials, prime))), 555 | add(client_key_pair.private, mult(scrambling, credentials)), 556 | prime 557 | ) 558 | end 559 | 560 | defp generate_server_premaster_secret( 561 | password_verifier, 562 | %KeyPair{} = server_key_pair, 563 | client_public_key, 564 | options 565 | ) 566 | when is_binary(password_verifier) and is_binary(client_public_key) do 567 | options = Keyword.merge(@default_options, options) 568 | prime_size = Keyword.get(options, :prime_size) 569 | hash_algorithm = Keyword.get(options, :hash_algorithm) 570 | 571 | %Group{prime: prime, prime_length: prime_length} = Group.get(prime_size) 572 | 573 | scrambling = 574 | hash( 575 | hash_algorithm, 576 | String.pad_leading(client_public_key, prime_length) <> 577 | String.pad_leading(server_key_pair.public, prime_length) 578 | ) 579 | 580 | mod_pow( 581 | mult( 582 | client_public_key, 583 | mod_pow( 584 | password_verifier, 585 | scrambling, 586 | prime 587 | ) 588 | ), 589 | server_key_pair.private, 590 | prime 591 | ) 592 | end 593 | 594 | defp generate_client_proof( 595 | client_public_key, 596 | server_public_key, 597 | premaster_secret, 598 | options 599 | ) do 600 | options = Keyword.merge(@default_options, options) 601 | hash_algorithm = Keyword.get(options, :hash_algorithm) 602 | 603 | hash( 604 | hash_algorithm, 605 | client_public_key <> server_public_key <> hash(hash_algorithm, premaster_secret) 606 | ) 607 | end 608 | 609 | defp generate_server_proof(client_proof, client_public_key, premaster_secret, options) do 610 | options = Keyword.merge(@default_options, options) 611 | hash_algorithm = Keyword.get(options, :hash_algorithm) 612 | 613 | hash( 614 | hash_algorithm, 615 | client_public_key <> client_proof <> hash(hash_algorithm, premaster_secret) 616 | ) 617 | end 618 | 619 | defp hash(type, value) when type in [:sha224, :sha256, :sha384, :sha512, :sha, :md5, :md4] do 620 | :crypto.hash(type, value) 621 | end 622 | 623 | defp random(bytes_quantity) when is_integer(bytes_quantity) do 624 | :crypto.strong_rand_bytes(bytes_quantity) 625 | end 626 | end 627 | -------------------------------------------------------------------------------- /lib/srp/client.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.Client do 2 | @moduledoc """ 3 | Defines a SRP client. 4 | 5 | ```elixir 6 | defmodule MyApp.SRP.Client do 7 | use SRP.Client 8 | end 9 | ``` 10 | 11 | It accepts a `prime_size` and a `hash_algorithm` as options. 12 | 13 | ```elixir 14 | defmodule MyApp.SRP.ClientWithOptions do 15 | use SRP.Client, prime_size: 8192, hash_algorithm: :sha512 16 | end 17 | ``` 18 | """ 19 | 20 | @doc """ 21 | See more information at `SRP.generate_verifier/2`. 22 | """ 23 | @callback generate_verifier(Identity.t()) :: IdentityVerifier.t() 24 | 25 | @doc """ 26 | See more information at `SRP.client_key_pair/1`. 27 | """ 28 | @callback key_pair :: KeyPair.t() 29 | 30 | @doc """ 31 | See more information at `SRP.client_proof/5`. 32 | """ 33 | @callback proof(binary(), binary(), KeyPair.t(), binary()) :: binary() 34 | 35 | @doc """ 36 | See more information at `SRP.valid_server_proof?/6`. 37 | """ 38 | @callback valid_server_proof?(binary(), Identity.t(), binary(), KeyPair.t(), binary()) :: 39 | boolean() 40 | 41 | defmacro __using__(opts) do 42 | quote do 43 | @behaviour SRP.Client 44 | 45 | @impl true 46 | def key_pair, do: SRP.client_key_pair(unquote(opts)) 47 | 48 | @impl true 49 | def generate_verifier(identity) do 50 | SRP.generate_verifier(identity, unquote(opts)) 51 | end 52 | 53 | @impl true 54 | def proof( 55 | identity, 56 | salt, 57 | client_key_pair, 58 | server_public_key 59 | ) do 60 | SRP.client_proof(identity, salt, client_key_pair, server_public_key, unquote(opts)) 61 | end 62 | 63 | @impl true 64 | def valid_server_proof?( 65 | server_proof, 66 | identity, 67 | salt, 68 | client_key_pair, 69 | server_public_key 70 | ) do 71 | SRP.valid_server_proof?( 72 | server_proof, 73 | identity, 74 | salt, 75 | client_key_pair, 76 | server_public_key, 77 | unquote(opts) 78 | ) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/srp/group.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.Group do 2 | @moduledoc false 3 | defstruct [:prime, :generator, :prime_length] 4 | 5 | @groups [ 6 | { 7 | 1024, 8 | """ 9 | EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C 10 | 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 11 | 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 12 | 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A 13 | FD5138FE 8376435B 9FC61D2F C0EB06E3 14 | """, 15 | 2 16 | }, 17 | { 18 | 1536, 19 | """ 20 | 9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961 21 | 4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843 22 | 80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B 23 | E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5 24 | 6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A 25 | F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E 26 | 8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB 27 | """, 28 | 2 29 | }, 30 | { 31 | 2048, 32 | """ 33 | AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 34 | 3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D 35 | CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB 36 | D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74 37 | 7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A 38 | 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D 39 | 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73 40 | 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6 41 | 94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F 42 | 9E4AFF73 43 | """, 44 | 2 45 | }, 46 | { 47 | 3072, 48 | """ 49 | FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 50 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 51 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 52 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 53 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 54 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 55 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 56 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 57 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 58 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 59 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 60 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 61 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 62 | E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF 63 | """, 64 | 5 65 | }, 66 | { 67 | 4096, 68 | """ 69 | FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 70 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 71 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 72 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 73 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 74 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 75 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 76 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 77 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 78 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 79 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 80 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 81 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 82 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 83 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 84 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 85 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 86 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 87 | FFFFFFFF FFFFFFFF 88 | """, 89 | 5 90 | }, 91 | { 92 | 6144, 93 | """ 94 | FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 95 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 96 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 97 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 98 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 99 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 100 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 101 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 102 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 103 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 104 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 105 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 106 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 107 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 108 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 109 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 110 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 111 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 112 | 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 113 | AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 114 | DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 115 | 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 116 | F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F 117 | BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA 118 | CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B 119 | B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 120 | 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E 121 | 6DCC4024 FFFFFFFF FFFFFFFF 122 | """, 123 | 5 124 | }, 125 | { 126 | 8192, 127 | """ 128 | 129 | FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 130 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 131 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 132 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 133 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 134 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 135 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 136 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 137 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 138 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 139 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 140 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 141 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 142 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 143 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 144 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 145 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 146 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 147 | 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 148 | AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 149 | DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 150 | 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 151 | F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F 152 | BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA 153 | CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B 154 | B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 155 | 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E 156 | 6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA 157 | 3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 158 | 5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9 159 | 22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886 160 | 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6 161 | 6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5 162 | 0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268 163 | 359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6 164 | FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71 165 | 60C980DD 98EDD3DF FFFFFFFF FFFFFFFF 166 | """, 167 | 19 168 | } 169 | ] 170 | 171 | Module.register_attribute(__MODULE__, :valid_sizes, accumulate: true) 172 | 173 | for {size, prime, generator} <- @groups do 174 | Module.put_attribute(__MODULE__, :valid_sizes, size) 175 | 176 | decoded_prime = 177 | prime 178 | |> String.replace(~r/\s/m, "") 179 | |> String.upcase() 180 | |> Base.decode16!() 181 | 182 | encoded_generator = :binary.encode_unsigned(generator) 183 | prime_length = String.length(decoded_prime) 184 | 185 | def get(unquote(size)) do 186 | %__MODULE__{ 187 | prime: unquote(decoded_prime), 188 | generator: unquote(encoded_generator), 189 | prime_length: unquote(prime_length) 190 | } 191 | end 192 | end 193 | 194 | defmacro valid_sizes, do: @valid_sizes 195 | end 196 | -------------------------------------------------------------------------------- /lib/srp/identity.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.Identity do 2 | @moduledoc """ 3 | A user identity is a struct formed by a username and a password. 4 | This identity is known only to the client. The server knowns only the `SRP.IdentityVerifier`. 5 | """ 6 | defstruct [:username, :password] 7 | 8 | @type t :: %__MODULE__{username: String.t(), password: String.t()} 9 | end 10 | -------------------------------------------------------------------------------- /lib/srp/identity_verifier.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.IdentityVerifier do 2 | @moduledoc """ 3 | A user identity verifier. 4 | This verifier is formed by the username, a random salt generated at registration time, 5 | and a password verifier derived from the user password. 6 | """ 7 | 8 | @enforce_keys [:username, :salt, :password_verifier] 9 | defstruct [:username, :salt, :password_verifier] 10 | 11 | @type t :: %__MODULE__{username: String.t(), salt: binary(), password_verifier: binary()} 12 | 13 | @doc false 14 | def new(username, salt, password_verifier) 15 | when is_binary(username) and is_binary(salt) and is_binary(password_verifier) do 16 | %__MODULE__{ 17 | username: username, 18 | salt: salt, 19 | password_verifier: password_verifier 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/srp/key_pair.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.KeyPair do 2 | @moduledoc """ 3 | A pair of ephemeral keys, one public and one private. 4 | The private key is random and the public is derived from the private. 5 | This keys are exchanged during the process of authentication. 6 | """ 7 | @enforce_keys [:private, :public] 8 | defstruct [:private, :public] 9 | 10 | @type t :: %__MODULE__{private: binary(), public: binary()} 11 | end 12 | -------------------------------------------------------------------------------- /lib/srp/math.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.Math do 2 | @moduledoc false 3 | 4 | def sub(left, right) when is_binary(left) do 5 | sub(:binary.decode_unsigned(left), right) 6 | end 7 | 8 | def sub(left, right) when is_binary(right) do 9 | sub(left, :binary.decode_unsigned(right)) 10 | end 11 | 12 | def sub(left, right) when is_integer(left) and is_integer(right) do 13 | left - right 14 | end 15 | 16 | def mult(left, right) when is_binary(left) do 17 | mult(:binary.decode_unsigned(left), right) 18 | end 19 | 20 | def mult(left, right) when is_binary(right) do 21 | mult(left, :binary.decode_unsigned(right)) 22 | end 23 | 24 | def mult(left, right) when is_integer(left) and is_integer(right) do 25 | left * right 26 | end 27 | 28 | def add(left, right) when is_binary(left) do 29 | add(:binary.decode_unsigned(left), right) 30 | end 31 | 32 | def add(left, right) when is_binary(right) do 33 | add(left, :binary.decode_unsigned(right)) 34 | end 35 | 36 | def add(left, right) when is_integer(left) and is_integer(right) do 37 | left + right 38 | end 39 | 40 | defdelegate mod_pow(value, exponent, modulus), to: :crypto 41 | end 42 | -------------------------------------------------------------------------------- /lib/srp/server.ex: -------------------------------------------------------------------------------- 1 | defmodule SRP.Server do 2 | @moduledoc """ 3 | Defines a SRP server. 4 | 5 | ```elixir 6 | defmodule MyApp.SRP.Server do 7 | use SRP.Server 8 | end 9 | ``` 10 | 11 | It accepts a `prime_size` and a `hash_algorithm` as options. 12 | 13 | ```elixir 14 | defmodule MyApp.SRP.ClientWithOptions do 15 | use SRP.Server, prime_size: 8192, hash_algorithm: :sha512 16 | end 17 | ``` 18 | """ 19 | 20 | @doc """ 21 | See more information at `SRP.server_key_pair/2`. 22 | """ 23 | @callback key_pair(binary()) :: KeyPair.t() 24 | 25 | @doc """ 26 | See more information at `SRP.server_proof/5`. 27 | """ 28 | @callback proof(binary(), binary(), KeyPair.t(), binary()) :: binary() 29 | 30 | @doc """ 31 | See more information at `SRP.valid_client_proof?/5`. 32 | """ 33 | @callback valid_client_proof?(binary(), binary(), KeyPair.t(), binary()) :: boolean() 34 | 35 | defmacro __using__(opts \\ []) do 36 | quote do 37 | @behaviour SRP.Server 38 | 39 | @impl true 40 | def key_pair(password_verifier), do: SRP.server_key_pair(password_verifier, unquote(opts)) 41 | 42 | @impl true 43 | def proof( 44 | client_proof, 45 | password_verifier, 46 | server_key_pair, 47 | client_public_key 48 | ) do 49 | SRP.server_proof( 50 | client_proof, 51 | password_verifier, 52 | server_key_pair, 53 | client_public_key, 54 | unquote(opts) 55 | ) 56 | end 57 | 58 | @impl true 59 | def valid_client_proof?( 60 | client_proof, 61 | password_verifier, 62 | server_key_pair, 63 | client_public_key 64 | ) do 65 | SRP.valid_client_proof?( 66 | client_proof, 67 | password_verifier, 68 | server_key_pair, 69 | client_public_key, 70 | unquote(opts) 71 | ) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Srp.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :srp, 7 | version: "0.2.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | test_coverage: [tool: ExCoveralls], 12 | preferred_cli_env: [coveralls: :test, "coveralls.travis": :test, "coveralls.html": :test], 13 | name: "SRP", 14 | source_url: "https://github.com/thiamsantos/srp-elixir", 15 | docs: docs(), 16 | package: package(), 17 | description: "Implementation of the Secure Remote Password Protocol" 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | extra_applications: [:crypto] 24 | ] 25 | end 26 | 27 | defp docs do 28 | [ 29 | main: "SRP" 30 | ] 31 | end 32 | 33 | defp package do 34 | [ 35 | name: "srp", 36 | licenses: ["Apache 2.0"], 37 | links: %{"GitHub" => "https://github.com/thiamsantos/srp-elixir"} 38 | ] 39 | end 40 | 41 | defp deps do 42 | [ 43 | {:credo, "~> 0.10.0", only: [:dev, :test], runtime: false}, 44 | {:excoveralls, "~> 0.10.1", only: :test, runtime: false}, 45 | {:ex_doc, "~> 0.18.0", only: :dev, runtime: false}, 46 | {:stream_data, "~> 0.4.2", only: :test} 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 6 | "ex_doc": {:hex, :ex_doc, "0.18.4", "4406b8891cecf1352f49975c6d554e62e4341ceb41b9338949077b0d4a97b949", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "excoveralls": {:hex, :excoveralls, "0.10.1", "407d50ac8fc63dfee9175ccb4548e6c5512b5052afa63eedb9cd452a32a91495", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 12 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 13 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 14 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 15 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 16 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 17 | "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm"}, 18 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 19 | } 20 | -------------------------------------------------------------------------------- /test/srp/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SRP.ClientTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest SRP.Client 5 | 6 | defmodule SRPClient do 7 | use SRP.Client 8 | end 9 | 10 | defmodule SRPClientWithOptions do 11 | use SRP.Client, prime_size: 8192, hash_algorithm: :sha512 12 | end 13 | 14 | describe "support srp client" do 15 | test "should generate premaster key" do 16 | identity = SRP.new_identity("alice", "password123") 17 | 18 | register = SRPClient.generate_verifier(identity) 19 | client_key_pair = SRPClient.key_pair() 20 | server_key_pair = SRP.server_key_pair(register.password_verifier) 21 | 22 | client_proof = 23 | SRPClient.proof( 24 | identity, 25 | register.salt, 26 | client_key_pair, 27 | server_key_pair.public 28 | ) 29 | 30 | assert SRP.valid_client_proof?( 31 | client_proof, 32 | register.password_verifier, 33 | server_key_pair, 34 | client_key_pair.public 35 | ) == true 36 | 37 | server_proof = 38 | SRP.server_proof( 39 | client_proof, 40 | register.password_verifier, 41 | server_key_pair, 42 | client_key_pair.public 43 | ) 44 | 45 | assert SRPClient.valid_server_proof?( 46 | server_proof, 47 | identity, 48 | register.salt, 49 | client_key_pair, 50 | server_key_pair.public 51 | ) == true 52 | end 53 | end 54 | 55 | describe "support srp client with options" do 56 | test "should generate premaster key" do 57 | options = [prime_size: 8192, hash_algorithm: :sha512] 58 | identity = SRP.new_identity("alice", "password123") 59 | register = SRPClientWithOptions.generate_verifier(identity) 60 | client_key_pair = SRPClientWithOptions.key_pair() 61 | server_key_pair = SRP.server_key_pair(register.password_verifier, options) 62 | 63 | client_proof = 64 | SRPClientWithOptions.proof( 65 | identity, 66 | register.salt, 67 | client_key_pair, 68 | server_key_pair.public 69 | ) 70 | 71 | assert SRP.valid_client_proof?( 72 | client_proof, 73 | register.password_verifier, 74 | server_key_pair, 75 | client_key_pair.public, 76 | options 77 | ) == true 78 | 79 | server_proof = 80 | SRP.server_proof( 81 | client_proof, 82 | register.password_verifier, 83 | server_key_pair, 84 | client_key_pair.public, 85 | options 86 | ) 87 | 88 | assert SRPClientWithOptions.valid_server_proof?( 89 | server_proof, 90 | identity, 91 | register.salt, 92 | client_key_pair, 93 | server_key_pair.public 94 | ) == true 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/srp/identity_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SRP.IdentityTest do 2 | use ExUnit.Case, async: true 3 | doctest SRP.Identity 4 | end 5 | -------------------------------------------------------------------------------- /test/srp/identity_verifier_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SRP.IdentityVerifierTest do 2 | use ExUnit.Case, async: true 3 | doctest SRP.IdentityVerifier 4 | end 5 | -------------------------------------------------------------------------------- /test/srp/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SRP.ServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest SRP.Server 5 | 6 | defmodule SRPServer do 7 | use SRP.Server 8 | end 9 | 10 | defmodule SRPServerWithOptions do 11 | use SRP.Server, prime_size: 8192, hash_algorithm: :sha512 12 | end 13 | 14 | describe "support srp client" do 15 | test "should generate premaster key" do 16 | identity = SRP.new_identity("alice", "password123") 17 | 18 | register = SRP.generate_verifier(identity) 19 | client_key_pair = SRP.client_key_pair() 20 | server_key_pair = SRPServer.key_pair(register.password_verifier) 21 | 22 | client_proof = 23 | SRP.client_proof( 24 | identity, 25 | register.salt, 26 | client_key_pair, 27 | server_key_pair.public 28 | ) 29 | 30 | assert SRPServer.valid_client_proof?( 31 | client_proof, 32 | register.password_verifier, 33 | server_key_pair, 34 | client_key_pair.public 35 | ) == true 36 | 37 | server_proof = 38 | SRPServer.proof( 39 | client_proof, 40 | register.password_verifier, 41 | server_key_pair, 42 | client_key_pair.public 43 | ) 44 | 45 | assert SRP.valid_server_proof?( 46 | server_proof, 47 | identity, 48 | register.salt, 49 | client_key_pair, 50 | server_key_pair.public 51 | ) == true 52 | end 53 | end 54 | 55 | describe "support srp client with options" do 56 | test "should generate premaster key" do 57 | options = [prime_size: 8192, hash_algorithm: :sha512] 58 | identity = SRP.new_identity("alice", "password123") 59 | 60 | register = SRP.generate_verifier(identity, options) 61 | client_key_pair = SRP.client_key_pair(options) 62 | server_key_pair = SRPServerWithOptions.key_pair(register.password_verifier) 63 | 64 | client_proof = 65 | SRP.client_proof( 66 | identity, 67 | register.salt, 68 | client_key_pair, 69 | server_key_pair.public, 70 | options 71 | ) 72 | 73 | assert SRPServerWithOptions.valid_client_proof?( 74 | client_proof, 75 | register.password_verifier, 76 | server_key_pair, 77 | client_key_pair.public 78 | ) == true 79 | 80 | server_proof = 81 | SRPServerWithOptions.proof( 82 | client_proof, 83 | register.password_verifier, 84 | server_key_pair, 85 | client_key_pair.public 86 | ) 87 | 88 | assert SRP.valid_server_proof?( 89 | server_proof, 90 | identity, 91 | register.salt, 92 | client_key_pair, 93 | server_key_pair.public, 94 | options 95 | ) == true 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/srp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SRPTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | doctest SRP 5 | 6 | alias SRP.Group 7 | require SRP.Group 8 | 9 | describe "srp" do 10 | test "generate same premaster key on client and server" do 11 | identity = SRP.new_identity("alice", "password123") 12 | 13 | register = SRP.generate_verifier(identity) 14 | server_key_pair = SRP.server_key_pair(register.password_verifier) 15 | client_key_pair = SRP.client_key_pair() 16 | 17 | client_proof = 18 | SRP.client_proof( 19 | identity, 20 | register.salt, 21 | client_key_pair, 22 | server_key_pair.public 23 | ) 24 | 25 | assert SRP.valid_client_proof?( 26 | client_proof, 27 | register.password_verifier, 28 | server_key_pair, 29 | client_key_pair.public 30 | ) == true 31 | 32 | server_proof = 33 | SRP.server_proof( 34 | client_proof, 35 | register.password_verifier, 36 | server_key_pair, 37 | client_key_pair.public 38 | ) 39 | 40 | assert SRP.valid_server_proof?( 41 | server_proof, 42 | identity, 43 | register.salt, 44 | client_key_pair, 45 | server_key_pair.public 46 | ) == true 47 | end 48 | end 49 | 50 | @tag :property 51 | describe "property tests" do 52 | property "srp" do 53 | check all username <- StreamData.string(:alphanumeric), 54 | password <- StreamData.string(:alphanumeric), 55 | hash_algorithm <- 56 | StreamData.member_of([:sha224, :sha256, :sha384, :sha, :md5, :md4, :sha512]), 57 | prime_size <- StreamData.member_of(Group.valid_sizes()), 58 | random_bytes <- StreamData.integer(32..2048) do 59 | options = [ 60 | hash_algorithm: hash_algorithm, 61 | prime_size: prime_size, 62 | random_bytes: random_bytes 63 | ] 64 | 65 | identity = SRP.new_identity(username, password) 66 | 67 | register = SRP.generate_verifier(identity, options) 68 | server_key_pair = SRP.server_key_pair(register.password_verifier, options) 69 | client_key_pair = SRP.client_key_pair(options) 70 | 71 | client_proof = 72 | SRP.client_proof( 73 | identity, 74 | register.salt, 75 | client_key_pair, 76 | server_key_pair.public, 77 | options 78 | ) 79 | 80 | assert SRP.valid_client_proof?( 81 | client_proof, 82 | register.password_verifier, 83 | server_key_pair, 84 | client_key_pair.public, 85 | options 86 | ) == true 87 | 88 | server_proof = 89 | SRP.server_proof( 90 | client_proof, 91 | register.password_verifier, 92 | server_key_pair, 93 | client_key_pair.public, 94 | options 95 | ) 96 | 97 | assert SRP.valid_server_proof?( 98 | server_proof, 99 | identity, 100 | register.salt, 101 | client_key_pair, 102 | server_key_pair.public, 103 | options 104 | ) == true 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------