├── .formatter.exs ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── general-issue.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE_v5.md ├── lib ├── comeonin.ex └── comeonin │ ├── behaviour_test_helper.ex │ └── password_hash.ex ├── mix.exs ├── mix.lock └── test ├── behaviour_test_helper_test.exs ├── comeonin_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: riverrun 7 | 8 | --- 9 | 10 | ### Environment 11 | 12 | * Elixir & Erlang/OTP versions (elixir --version): 13 | * Operating system: 14 | 15 | ### Current behavior 16 | 17 | Include code samples, errors and stacktraces if appropriate. 18 | 19 | ### Expected behavior 20 | 21 | A short description on how you expect the code to behave. 22 | 23 | ### Additional information 24 | 25 | Add any other information about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: riverrun 7 | 8 | --- 9 | 10 | ### Problem 11 | 12 | Describe the problem you are facing - if there is no problem, leave this blank. 13 | 14 | ### Solution 15 | 16 | Describe the solution to the problem - or your feature request. 17 | 18 | ### Additional info 19 | 20 | Add any more information about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General issue 3 | about: Issue that is not a bug or feature request 4 | title: '' 5 | labels: '' 6 | assignees: riverrun 7 | 8 | --- 9 | 10 | ### Environment 11 | 12 | * Elixir & Erlang/OTP versions (elixir --version): 13 | * Operating system: 14 | 15 | ### Issue 16 | 17 | Describe the issue here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /priv/ 6 | /.fetch 7 | erl_crash.dump 8 | *.ez 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.7 5 | - 1.8 6 | 7 | otp_release: 8 | - 21.1 9 | 10 | script: 11 | - mix compile --warnings-as-errors 12 | - mix format --check-formatted 13 | - mix test 14 | - mix dialyzer --halt-exit-status 15 | 16 | cache: 17 | directories: 18 | - priv/plts 19 | 20 | sudo: false 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v5.5.0 (2024-10-04) 4 | 5 | * Changes 6 | * Updated dependencies and made changes to silence warnings in Elixir 1.17 7 | 8 | ## v5.4.0 9 | 10 | * Changes 11 | * Added deprecation warnings to add_hash and check_pass 12 | 13 | ## v5.3.3 14 | 15 | * Changes 16 | * updated documentation and README 17 | 18 | ## v5.3.0 19 | 20 | * Changes 21 | * changed `add_hash` so that it does NOT set the password to nil 22 | 23 | ## v5.2.0 24 | 25 | * Enhancements 26 | * updated documentation for the Comeonin module 27 | 28 | ## v5.1.0 29 | 30 | * Enhancements 31 | * add a behaviour_test module to be used by implementations of the Comeonin behaviours 32 | 33 | ## v5.0.0 34 | 35 | * Enhancements 36 | * updated Comeonin to be a specification for password hashing libraries 37 | * it offers the Comeonin and Comeonin.PasswordHash behaviours 38 | 39 | ## v4.1.2 40 | 41 | * Bug fixes 42 | * made sure that the `hash_key` option is used if present 43 | 44 | ## v4.1.1 45 | 46 | * Changes 47 | * changed `check_pass` with statement to case - in an attempt to suppress dialyzer warnings 48 | 49 | ## v4.1.0 50 | 51 | * Enhancements 52 | * added `hash_key` option to `check_pass` 53 | * as with version 4.0, `check_pass` will use `:password_hash` / `:encrypted_password` if present 54 | 55 | ## v4.0.0 56 | 57 | * Enhancements 58 | * Added support for Argon2 (as an optional dependency - argon2_elixir) 59 | * Added higher-level helper functions to each algorithm's module 60 | * These functions accept / return maps and should reduce code use when adding password hashes / checking passwords 61 | * Improved the statistics function in each module - `report` 62 | * Changes 63 | * Made all the hashing algorithms optional dependencies 64 | * Moved the configuration to the separate dependency libraries 65 | * Removed support for one-time passwords 66 | * This is now a separate library - OneTimePassEcto ("https://github.com/riverrun/one_time_pass_ecto") 67 | 68 | ## v3.2.0 69 | 70 | * Bug fixes 71 | * Shortening user-supplied salts to 128 bits - making it compatible with other implementations 72 | 73 | ## v3.1.0 74 | 75 | * Changes 76 | * Now forcing a recompile of C code for Windows users 77 | 78 | ## v3.0.0 79 | 80 | * Enhancements 81 | * Small changes to NIF code to make it more scheduler-friendly 82 | * Improved documentation with respect to Argon2 83 | * Changes 84 | * Now using `elixir_make` to help compile the C code 85 | 86 | ## v2.6.0 87 | 88 | * Enhancements 89 | * Improved Windows build to prevent build failure when upgrading Erlang versions 90 | 91 | ## v2.4.0 92 | 93 | * Enhancements 94 | * Improved Windows build error messages 95 | * Changes 96 | * Changed behavior for incorrect input to Bcrypt.gen_salt 97 | 98 | ## v2.3.0 99 | 100 | * Enhancements 101 | * Added support for one-time passwords (HOTP, TOTP) for use in two factor authentication 102 | 103 | ## v2.2.0 104 | 105 | * Changes 106 | * Improved documentation for Windows builds 107 | * Updated the required Elixir version to 1.2 108 | 109 | ## v2.1.0 110 | 111 | * Enhancements 112 | * Added legacy option to Comeonin.Bcrypt.gen_salt so that hashes with the older $2a$ prefix can be more easily generated 113 | * To try to solve the load errors after upgrading Erlang / Elixir: 114 | * Force C code to be recompiled every time `deps.compile` is called 115 | * Added basic upgrade function to the NIF library 116 | * To be compatible with the nerves project: 117 | * Added CROSSCOMPILE option to the Makefile 118 | * Stopped the library autoloading on compilation (requires Elixir 1.2.2) 119 | 120 | ## v2.0.0 (2015-12-17) 121 | 122 | * Changes 123 | * Increased the default number of rounds for pbkdf2_sha512 to 100_000 124 | * Removed `create_hash` and `create_user` functions 125 | * Moved the password strength checker and random password generator to a separate package, called NotQwerty123 126 | * This means that i18n support has been moved to NotQwerty123 127 | 128 | ## v1.6.0 (2015-11-17) 129 | 130 | * Changes 131 | * Edited C code and deleted unused functions 132 | 133 | ## v1.5.0 (2015-11-10) 134 | 135 | * Changes 136 | * Moved gettext support to `comeonin_i18n` optional dependency 137 | * Removed forced compilation of C code in dev and prod environments 138 | 139 | ## v1.4.0 (2015-11-06) 140 | 141 | * Enhancements 142 | * Added gettext support 143 | * Added Japanese translations for messages 144 | 145 | ## v1.3.0 (2015-10-18) 146 | 147 | * Enhancements 148 | * Improved the efficiency of the common password check for the `strong_password` function 149 | * Added more information to the Mix build errors 150 | * Changes 151 | * Forcing compilation of C code in dev and prod environments 152 | 153 | ## v1.2.0 (2015-09-26) 154 | 155 | * Enhancements 156 | * Added a common option to the `strong_password` check. This checks for passwords that are easy to guess, or common 157 | * Improved random password generator - added a check to ensure it is strong and set the minimum length to 8 characters 158 | 159 | ## v1.1.4 (2015-09-25) 160 | 161 | * Bug fix 162 | * Removed `random_bytes` function. Now calling :crypto.strong_rand_bytes directly 163 | 164 | ## v1.1.0 (2015-07-28) 165 | 166 | * Changes 167 | * Divided the `strong password` check into two parts: minimum length and check for punctuation 168 | characters and digits 169 | * Removed configuration values for password length for generated passwords and minimum length of passwords 170 | for the password check 171 | 172 | ## v1.0.5 (2015-07-14) 173 | 174 | * Bug fix 175 | * Replaced `Mix.Shell.info` with `IO.binwrite` to prevent compile errors with certain character encodings 176 | 177 | ## v1.0.1 (2015-05-31) 178 | 179 | * Enhancements 180 | * Enabled the create_user function to be used with atoms as keys as well as strings 181 | 182 | ## v1.0.0 (2015-05-20) 183 | 184 | * Changes 185 | * Renamed signup_user, function to check password strength before hashing the password, to create_user 186 | * Enhancements 187 | * Added create_user function which takes a map, removes the "password" entry and adds a "password_hash" entry 188 | 189 | ## v0.11.0 (2015-05-19) 190 | 191 | * Changes 192 | * Renamed hashpwsalt/2 to signup_user/1 193 | 194 | ## v0.10.0 (2015-05-14) 195 | 196 | * Changes 197 | * Removed log_rounds, or rounds, parameter for the function hashpwsalt 198 | * Added option to check password (for strength) in the function hashpwsalt (hashpwsalt/2) 199 | 200 | ## v0.9.0 (2015-05-08) 201 | 202 | * Enhancements 203 | * Added random password generator 204 | * Added optional check to test if passwords have digits and punctuation characters 205 | * Bug fixes 206 | * Added information about password strength and password policies to the documentation 207 | 208 | ## v0.8.2 (2015-05-02) 209 | 210 | * Bug fixes 211 | * Updated Windows build and improved error information at compile time 212 | 213 | ## v0.8.0 (2015-04-20) 214 | 215 | * Bug fixes 216 | * Updated bcrypt to support non-ascii characters in the password (pbkdf2 already supports these characters) 217 | 218 | ## v0.7.0 (2015-04-18) 219 | 220 | * Enhancements 221 | * Use crypto.strong_rand_bytes by default for generating random numbers 222 | 223 | ## v0.6.0 (2015-04-17) 224 | 225 | * Enhancements 226 | * Updated bcrypt implementation to only call C functions for the most expensive operations 227 | 228 | ## v0.5.0 (2015-04-14) 229 | 230 | * Enhancements 231 | * Updated bcrypt implementation so that long-running NIFs are cut to a minimum 232 | 233 | ## v0.4.0 (2015-04-05) 234 | 235 | * Enhancements 236 | * Updated pbkdf2_sha512 to prevent users from calling `hashpass` without a salt 237 | 238 | ## v0.3.0 (2015-03-04) 239 | 240 | * Enhancements 241 | * Updated bcrypt to version 1.5.2 242 | 243 | ## v0.2.4 (2015-02-26) 244 | 245 | * Enhancements 246 | * Added configuration options for number of log_rounds, or rounds 247 | 248 | ## v0.2.2 (2015-01-25) 249 | 250 | * Enhancements 251 | * Improved documentation about the recommended time the functions should take 252 | * Increased default number of rounds for pbkdf2_sha512 from 40000 to 60000 253 | * Improved implementation of dummy check 254 | 255 | * Changes 256 | * Removed the `salt_length` optional argument from `Comeonin.Pbkdf2.hashpwsalt`. The only optional argument to this function is now the number of rounds 257 | 258 | ## v0.2.0 (2015-01-21) 259 | 260 | * Enhancements 261 | * Added support for pbkdf2_sha512 262 | * Added Travis integration 263 | * Added timing functions to help developers adjust the complexity of the key derivation functions 264 | 265 | * Changes 266 | * Removed the hashing and check functions from the main Comeonin module 267 | 268 | ## v0.1.1 269 | 270 | * Bug fixes 271 | * Enable build on OS X 272 | 273 | ## v0.1.0 274 | 275 | * Bcrypt authentication 276 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All code in this application, unless otherwise stated, is subject 2 | to the following license: 3 | 4 | Copyright (c) 2014-2021 David Whitlock 5 | Some rights reserved. 6 | 7 | Redistribution and use in source and binary forms of the software as well as 8 | documentation, with or without modification, are permitted provided that the 9 | following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above 15 | copyright notice, this list of conditions and the following disclaimer in the 16 | documentation and/or other materials provided with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific prior written 20 | permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 28 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 29 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 30 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 31 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED 32 | OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comeonin 2 | 3 | [![Build Status](https://travis-ci.com/riverrun/comeonin.svg?branch=master)](https://travis-ci.com/riverrun/comeonin) 4 | [![Module Version](https://img.shields.io/hexpm/v/comeonin.svg)](https://hex.pm/packages/comeonin) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/comeonin/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/comeonin.svg)](https://hex.pm/packages/comeonin) 7 | [![License](https://img.shields.io/hexpm/l/comeonin.svg)](https://github.com/riverrun/comeonin/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/riverrun/comeonin.svg)](https://github.com/riverrun/comeonin/commits/master) 9 | [![Join the chat at https://gitter.im/comeonin/Lobby](https://badges.gitter.im/comeonin/Lobby.svg)](https://gitter.im/comeonin/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | Comeonin is a specification for password hashing libraries. 12 | 13 | For information about hashing passwords in your app, see 14 | [Password hashing libraries](#password-hashing-libraries). 15 | 16 | ## Changes in version 5 17 | 18 | In version 5.0 and above, Comeonin now provides two behaviours, Comeonin and 19 | Comeonin.PasswordHash, which password hash libraries then implement. 20 | 21 | With these changes, Comeonin is now a dependency of the password hashing 22 | library you choose to use, and in most cases, you will not use it 23 | directly. 24 | 25 | See the [UPGRADE_v5 guide](https://github.com/riverrun/comeonin/blob/master/UPGRADE_v5.md) 26 | for information about you can upgrade to version 5. 27 | 28 | ## Password hashing libraries 29 | 30 | The following libraries all implement the Comeonin and Comeonin.PasswordHash 31 | behaviours: 32 | 33 | * Argon2 - argon2_elixir 34 | * [docs](https://hexdocs.pm/argon2_elixir) 35 | * [source](https://github.com/riverrun/argon2_elixir) 36 | * Bcrypt - bcrypt_elixir 37 | * [docs](https://hexdocs.pm/bcrypt_elixir) 38 | * [source](https://github.com/riverrun/bcrypt_elixir) 39 | * Pbkdf2 - pbkdf2_elixir 40 | * [docs](https://hexdocs.pm/pbkdf2_elixir) 41 | * [source](https://github.com/riverrun/pbkdf2_elixir) 42 | 43 | Argon2 is currently considered to be the strongest password hashing function, 44 | and it is the one we recommend. 45 | 46 | Bcrypt and Pbkdf2 are viable alternatives, but they are less resistant than Argon2, 47 | to attacks using GPUs or dedicated hardware. 48 | 49 | ### Windows users 50 | 51 | On Windows, it can be time-consuming and problematic to setup the environment needed 52 | to compile the C code in Argon2 and Bcrypt. For this reason, it is often easier to install 53 | Pbkdf2, which has no C dependencies. 54 | 55 | For more information, see 56 | [Choosing a library](https://github.com/riverrun/comeonin/wiki/Choosing-the-password-hashing-library). 57 | 58 | ## Comeonin wiki 59 | 60 | See the [Comeonin wiki](https://github.com/riverrun/comeonin/wiki) for more 61 | information on the following topics: 62 | 63 | * [Hashing passwords](https://github.com/riverrun/comeonin/wiki/Hashing-passwords) - a general guide to hashing passwords in your Elixir app 64 | * [Password hashing libraries](https://github.com/riverrun/comeonin/wiki/Choosing-the-password-hashing-library) 65 | * [Requirements](https://github.com/riverrun/comeonin/wiki/Requirements) 66 | * [Deployment](https://github.com/riverrun/comeonin/wiki/Deployment) - including information about using Docker 67 | * [References](https://github.com/riverrun/comeonin/wiki/References) 68 | 69 | ## Contributing 70 | 71 | There are many ways you can contribute to the development of Comeonin, including: 72 | 73 | * Reporting issues 74 | * Improving documentation 75 | * Sharing your experiences with others 76 | 77 | ### License 78 | 79 | BSD. For full details, please read the LICENSE file. 80 | -------------------------------------------------------------------------------- /UPGRADE_v5.md: -------------------------------------------------------------------------------- 1 | # Upgrading to version 5 2 | 3 | In version 5 of Comeonin, in most cases, you will not call Comeonin directly, 4 | and so you will be able to remove it from your app. 5 | 6 | Follow the instructions below to upgrade: 7 | 8 | 1. Remove `:comeonin` from the `deps` function in your mix.exs file. 9 | 2. Update `:argon2_elixir` to version 2.0, `:bcrypt_elixir` to version 2.0, 10 | or `:pbkdf2_elixir` to version 1.0. 11 | 3. Using the conversion tables below, edit the hashing functions. 12 | 13 | | Comeonin v4 | Comeonin v5 | 14 | | :---------- | :---------- | 15 | | Comeonin.Argon2.add_hash | Argon2.add_hash | 16 | | Comeonin.Argon2.check_pass | Argon2.check_pass | 17 | | Comeonin.Argon2.hashpwsalt | Argon2.hash_pwd_salt | 18 | | Comeonin.Argon2.checkpw | Argon2.verify_pass | 19 | | Comeonin.Argon2.dummy_checkpw | Argon2.no_user_verify | 20 | 21 | | Comeonin v4 | Comeonin v5 | 22 | | :---------- | :---------- | 23 | | Comeonin.Bcrypt.add_hash | Bcrypt.add_hash | 24 | | Comeonin.Bcrypt.check_pass | Bcrypt.check_pass | 25 | | Comeonin.Bcrypt.hashpwsalt | Bcrypt.hash_pwd_salt | 26 | | Comeonin.Bcrypt.checkpw | Bcrypt.verify_pass | 27 | | Comeonin.Bcrypt.dummy_checkpw | Bcrypt.no_user_verify | 28 | 29 | | Comeonin v4 | Comeonin v5 | 30 | | :---------- | :---------- | 31 | | Comeonin.Pbkdf2.add_hash | Pbkdf2.add_hash | 32 | | Comeonin.Pbkdf2.check_pass | Pbkdf2.check_pass | 33 | | Comeonin.Pbkdf2.hashpwsalt | Pbkdf2.hash_pwd_salt | 34 | | Comeonin.Pbkdf2.checkpw | Pbkdf2.verify_pass | 35 | | Comeonin.Pbkdf2.dummy_checkpw | Pbkdf2.no_user_verify | 36 | -------------------------------------------------------------------------------- /lib/comeonin.ex: -------------------------------------------------------------------------------- 1 | defmodule Comeonin do 2 | @moduledoc """ 3 | Defines a behaviour for higher-level password hashing functions. 4 | """ 5 | 6 | @type opts :: keyword 7 | @type password :: binary 8 | @type user_struct :: map | nil 9 | 10 | @doc deprecated: "This function will be removed in the next major version." 11 | @callback add_hash(password, opts) :: map 12 | 13 | @doc deprecated: "This function will be removed in the next major version." 14 | @callback check_pass(user_struct, password, opts) :: {:ok, map} | {:error, String.t()} 15 | 16 | @doc """ 17 | Runs the password hash function, but always returns false. 18 | 19 | This function is intended to make it more difficult for any potential 20 | attacker to find valid usernames by using timing attacks. This function 21 | is only useful if it is used as part of a policy of hiding usernames. 22 | """ 23 | @callback no_user_verify(opts) :: false 24 | 25 | defmacro __using__(_) do 26 | quote do 27 | @behaviour Comeonin 28 | @behaviour Comeonin.PasswordHash 29 | 30 | @impl Comeonin 31 | @deprecated "Use hash_pwd_salt(password, opts) to generate a new hash and set it on the password_hash field" 32 | def add_hash(password, opts \\ []) do 33 | hash_key = opts[:hash_key] || :password_hash 34 | %{hash_key => hash_pwd_salt(password, opts)} 35 | end 36 | 37 | @impl Comeonin 38 | @deprecated "Use verify_pass(password, hash) instead, where hash is typically the value of the stored hash, such as user.password_hash" 39 | def check_pass(user, password, opts \\ []) 40 | 41 | def check_pass(nil, _password, opts) do 42 | unless opts[:hide_user] == false, do: no_user_verify(opts) 43 | {:error, "invalid user-identifier"} 44 | end 45 | 46 | def check_pass(user, password, opts) when is_binary(password) do 47 | case get_hash(user, opts[:hash_key]) do 48 | {:ok, hash} -> 49 | if verify_pass(password, hash), do: {:ok, user}, else: {:error, "invalid password"} 50 | 51 | _ -> 52 | {:error, "no password hash found in the user struct"} 53 | end 54 | end 55 | 56 | def check_pass(_, _, _) do 57 | {:error, "password is not a string"} 58 | end 59 | 60 | defp get_hash(%{password_hash: hash}, nil), do: {:ok, hash} 61 | defp get_hash(%{encrypted_password: hash}, nil), do: {:ok, hash} 62 | defp get_hash(_, nil), do: nil 63 | 64 | defp get_hash(user, hash_key) do 65 | if hash = Map.get(user, hash_key), do: {:ok, hash} 66 | end 67 | 68 | @doc """ 69 | Runs the password hash function, but always returns false. 70 | 71 | This function is intended to make it more difficult for any potential 72 | attacker to find valid usernames by using timing attacks. This function 73 | is only useful if it is used as part of a policy of hiding usernames. 74 | 75 | There are concerns about this function using too many resources (CPU and 76 | memory). An alternative approach is to create a function that adds a sleep 77 | calculated to make the time spent running the function the same as if the 78 | hash function was run. 79 | 80 | ## Options 81 | 82 | This function should be called with the same options as those used by 83 | `hash_pwd_salt/2`. 84 | """ 85 | @impl Comeonin 86 | def no_user_verify(opts \\ []) do 87 | hash_pwd_salt("", opts) 88 | false 89 | end 90 | 91 | defoverridable Comeonin 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/comeonin/behaviour_test_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Comeonin.BehaviourTestHelper do 2 | @moduledoc """ 3 | Test helper functions for Comeonin behaviours. 4 | """ 5 | 6 | @doc """ 7 | List of passwords that just contain basic ascii characters. 8 | """ 9 | def ascii_passwords do 10 | ["passw0rd", "hard2guess", "!@#$%^&* ", "1q2w3e4r5t"] 11 | end 12 | 13 | @doc """ 14 | List of passwords that contain non-ascii characters. 15 | """ 16 | def non_ascii_passwords do 17 | ["påsswörd", "aáåä eéê ëoôö", "мадам, я доктор, вот банан", "Я❤três☕ où☔"] 18 | end 19 | 20 | @doc """ 21 | Checks that the `verify_pass/2` function returns true for correct password. 22 | """ 23 | def correct_password_true(module, password) do 24 | module.verify_pass(password, module.hash_pwd_salt(password)) 25 | end 26 | 27 | @doc """ 28 | Checks that the `verify_pass/2` function returns false for incorrect passwords. 29 | """ 30 | def wrong_password_false(module, password) do 31 | hash = module.hash_pwd_salt(password) 32 | 33 | password 34 | |> wrong_passwords() 35 | |> Enum.all?(&(module.verify_pass(&1, hash) == false)) 36 | end 37 | 38 | @doc """ 39 | Checks that the `add_hash/2` function creates a map with the `password_hash` set. 40 | """ 41 | @doc deprecated: "This function will be removed in the next major version." 42 | def add_hash_creates_map(module, password) do 43 | %{password_hash: hash} = module.add_hash(password) 44 | module.verify_pass(password, hash) 45 | end 46 | 47 | @doc """ 48 | Checks that the `check_pass/3` function returns the user for correct passwords. 49 | """ 50 | @doc deprecated: "This function will be removed in the next major version." 51 | def check_pass_returns_user(module, password) do 52 | hash = module.hash_pwd_salt(password) 53 | user = %{id: 2, name: "fred", password_hash: hash} 54 | module.check_pass(user, password) == {:ok, user} 55 | end 56 | 57 | @doc """ 58 | Checks that the `check_pass/3` function returns an error for incorrect passwords. 59 | """ 60 | @doc deprecated: "This function will be removed in the next major version." 61 | def check_pass_returns_error(module, password) do 62 | hash = module.hash_pwd_salt(password) 63 | user = %{id: 2, name: "fred", password_hash: hash} 64 | 65 | password 66 | |> wrong_passwords() 67 | |> Enum.all?(&(module.check_pass(user, &1) == {:error, "invalid password"})) 68 | end 69 | 70 | @doc """ 71 | Checks that the `check_pass/3` function returns an error when no user is found. 72 | """ 73 | @doc deprecated: "This function will be removed in the next major version." 74 | def check_pass_nil_user(module) do 75 | module.check_pass(nil, "password") == {:error, "invalid user-identifier"} 76 | end 77 | 78 | defp wrong_passwords(password) do 79 | words = [password, String.duplicate(password, 2)] 80 | reversed = Enum.map(words, &String.reverse(&1)) 81 | Enum.flat_map(words ++ reversed, &slices/1) 82 | end 83 | 84 | defp slices(password) do 85 | ranges = [{1, -1}, {0, -2}, {2, -1}, {2, -2}] 86 | for {first, last} <- ranges, do: String.slice(password, first..last//1) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/comeonin/password_hash.ex: -------------------------------------------------------------------------------- 1 | defmodule Comeonin.PasswordHash do 2 | @moduledoc """ 3 | Defines a behaviour for password hashing functions. 4 | """ 5 | 6 | @type opts :: keyword 7 | @type password :: binary 8 | @type password_hash :: binary 9 | 10 | @doc """ 11 | Generates a random salt and then hashes the password. 12 | """ 13 | @callback hash_pwd_salt(password, opts) :: password_hash 14 | 15 | @doc """ 16 | Checks the password by comparing it with a stored hash. 17 | 18 | Please note that the first argument to `verify_pass` should be the 19 | password, and the second argument should be the password hash. 20 | """ 21 | @callback verify_pass(password, password_hash) :: boolean 22 | end 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Comeonin.Mixfile do 2 | use Mix.Project 3 | 4 | @version "5.5.1" 5 | @description "A specification for password hashing libraries" 6 | @source_url "https://github.com/riverrun/comeonin" 7 | 8 | def project do 9 | [ 10 | app: :comeonin, 11 | version: @version, 12 | elixir: "~> 1.7", 13 | start_permanent: Mix.env() == :prod, 14 | name: "Comeonin", 15 | description: @description, 16 | package: package(), 17 | deps: deps(), 18 | docs: docs(), 19 | dialyzer: [ 20 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"} 21 | ] 22 | ] 23 | end 24 | 25 | def application do 26 | [ 27 | extra_applications: [:logger] 28 | ] 29 | end 30 | 31 | defp deps do 32 | [ 33 | {:ex_doc, "~> 0.23", only: :dev, runtime: false}, 34 | {:dialyxir, "~> 1.3", only: :dev, runtime: false} 35 | ] 36 | end 37 | 38 | defp package do 39 | [ 40 | files: ["lib", "mix.exs", "CHANGELOG.md", "README.md", "LICENSE"], 41 | maintainers: ["David Whitlock"], 42 | licenses: ["BSD-3-Clause"], 43 | links: %{ 44 | "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md", 45 | "GitHub" => @source_url 46 | } 47 | ] 48 | end 49 | 50 | defp docs do 51 | [ 52 | main: "readme", 53 | source_ref: "v#{@version}", 54 | source_url: @source_url, 55 | extras: ["README.md", "UPGRADE_v5.md", "CHANGELOG.md"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 3 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 5 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 6 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 7 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 11 | } 12 | -------------------------------------------------------------------------------- /test/behaviour_test_helper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Comeonin.BehaviourTestHelperTest do 2 | use ExUnit.Case 3 | 4 | import Comeonin.BehaviourTestHelper 5 | 6 | test "implementation of Comeonin.PasswordHash behaviour" do 7 | password = Enum.random(ascii_passwords()) 8 | assert correct_password_true(Comeonin.TestHash, password) 9 | assert wrong_password_false(Comeonin.TestHash, password) 10 | refute correct_password_true(Comeonin.FailHash, password) 11 | refute wrong_password_false(Comeonin.FailHash, password) 12 | end 13 | 14 | test "Comeonin.PasswordHash behaviour with non-ascii characters" do 15 | password = Enum.random(non_ascii_passwords()) 16 | assert correct_password_true(Comeonin.TestHash, password) 17 | assert wrong_password_false(Comeonin.TestHash, password) 18 | refute correct_password_true(Comeonin.FailHash, password) 19 | refute wrong_password_false(Comeonin.FailHash, password) 20 | end 21 | 22 | test "add_hash function" do 23 | password = Enum.random(ascii_passwords()) 24 | assert add_hash_creates_map(Comeonin.TestHash, password) 25 | end 26 | 27 | test "check_pass function" do 28 | password = Enum.random(ascii_passwords()) 29 | assert check_pass_returns_user(Comeonin.TestHash, password) 30 | assert check_pass_returns_error(Comeonin.TestHash, password) 31 | refute check_pass_returns_error(Comeonin.FailHash, password) 32 | assert check_pass_nil_user(Comeonin.TestHash) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/comeonin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ComeoninTest do 2 | use ExUnit.Case 3 | 4 | alias Comeonin.{OverrideHash, TestHash} 5 | 6 | test "add_hash with default arguments" do 7 | assert %{password_hash: hash} = TestHash.add_hash("password") 8 | assert TestHash.verify_pass("password", hash) 9 | end 10 | 11 | test "add_hash with custom hash_key" do 12 | assert %{encrypted_password: hash} = 13 | TestHash.add_hash("password", hash_key: :encrypted_password) 14 | 15 | assert TestHash.verify_pass("password", hash) 16 | end 17 | 18 | test "check_pass with default arguments" do 19 | user = %{password_hash: TestHash.hash_pwd_salt("password")} 20 | assert {:ok, user_1} = TestHash.check_pass(user, "password") 21 | assert user_1 == user 22 | assert {:error, message} = TestHash.check_pass(nil, "password") 23 | assert message =~ "invalid user-identifier" 24 | user = %{password_hash: TestHash.hash_pwd_salt("password1")} 25 | assert {:error, message} = TestHash.check_pass(user, "password") 26 | assert message =~ "invalid password" 27 | end 28 | 29 | test "check_pass with custom hash_key" do 30 | user = %{encrypted_password: TestHash.hash_pwd_salt("password")} 31 | assert {:ok, user_1} = TestHash.check_pass(user, "password") 32 | assert user_1 == user 33 | user = %{arrr: TestHash.hash_pwd_salt("password")} 34 | assert {:ok, user_1} = TestHash.check_pass(user, "password", hash_key: :arrr) 35 | assert user_1 == user 36 | user = %{arrrggghh: TestHash.hash_pwd_salt("password")} 37 | assert {:error, message} = TestHash.check_pass(user, "password") 38 | assert message =~ "no password hash found in the user struct" 39 | end 40 | 41 | test "can override add_hash" do 42 | assert %{password_hash: hash, password: message} = OverrideHash.add_hash("password") 43 | assert OverrideHash.verify_pass("password", hash) 44 | assert message =~ "FILTERED" 45 | end 46 | 47 | test "can override check_pass" do 48 | user = %{password_hash: OverrideHash.hash_pwd_salt("password")} 49 | assert {:ok, user_1} = OverrideHash.check_pass(user, "password") 50 | assert user_1 == %{} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule Comeonin.TestHash do 4 | use Comeonin 5 | 6 | @impl true 7 | def hash_pwd_salt(password, _opts \\ []) do 8 | password 9 | end 10 | 11 | @impl true 12 | def verify_pass(password, hash) do 13 | password == hash 14 | end 15 | end 16 | 17 | defmodule Comeonin.FailHash do 18 | use Comeonin 19 | 20 | @impl true 21 | def hash_pwd_salt(password, _opts \\ []) do 22 | password 23 | end 24 | 25 | @impl true 26 | def verify_pass(password, hash) do 27 | password != hash 28 | end 29 | end 30 | 31 | defmodule Comeonin.OverrideHash do 32 | use Comeonin 33 | 34 | @impl true 35 | def add_hash(password, opts) do 36 | hash_key = opts[:hash_key] || :password_hash 37 | %{hash_key => hash_pwd_salt(password, opts), :password => "FILTERED"} 38 | end 39 | 40 | @impl true 41 | def check_pass(user, password, opts) do 42 | with {:ok, user} <- super(user, password, opts), 43 | do: {:ok, Map.drop(user, [:password_hash])} 44 | end 45 | 46 | @impl true 47 | def hash_pwd_salt(password, _opts \\ []) do 48 | password 49 | end 50 | 51 | @impl true 52 | def verify_pass(password, hash) do 53 | password == hash 54 | end 55 | end 56 | --------------------------------------------------------------------------------