From 94114f079a759dc1aaac20dd6320c61be3088fde Mon Sep 17 00:00:00 2001 From: David Whitlock Date: Tue, 12 Feb 2019 11:22:52 +0700 Subject: [PATCH] Updated README and documentation --- .formatter.exs | 4 ++ CHANGELOG.md | 16 ++++++++ README.md | 60 +++++++++------------------ lib/bcrypt.ex | 98 +++++++++++++++++++++----------------------- lib/bcrypt/stats.ex | 5 ++- mix.exs | 5 ++- mix.lock | 1 + test/bcrypt_test.exs | 84 ++++++++++++++++++++++++++----------- test/test_helper.exs | 33 +++++++++++++++ 9 files changed, 186 insertions(+), 120 deletions(-) create mode 100644 .formatter.exs create mode 100644 CHANGELOG.md diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d8debc3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## v2.0.0 + +* Enhancements + * Updated to use the Comeonin and Comeonin.PasswordHash behaviours (Comeonin v5.0) + +## v1.0.0 + +* Enhancements + * Updated C NIF code to use dirty schedulers + +## v0.12.0 + +* Changes + * Created separate Bcrypt library diff --git a/README.md b/README.md index b372634..b6734ee 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ # Bcrypt -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/alovedalongthe) [![Hex.pm Version](http://img.shields.io/hexpm/v/bcrypt_elixir.svg)](https://hex.pm/packages/bcrypt_elixir) -Bcrypt password hashing algorithm for Elixir. +Bcrypt password hashing library for Elixir. Bcrypt is a well-tested password-based key derivation function that can be configured to remain slow and resistant to brute-force attacks even as computational power increases. -This version is based on the OpenBSD version of Bcrypt and supports -the `$2b$` and `$2a$` prefixes. For advice on how to use hashes with -the `$2y$` prefix, see [this issue](https://github.com/riverrun/comeonin/issues/103). - -This library can be used on its own, or it can be used together -with [Comeonin](https://hexdocs.pm/comeonin/api-reference.html), -which provides a higher-level api. - ## Installation 1. Add bcrypt_elixir to the `deps` section of your mix.exs file: @@ -26,12 +17,12 @@ If you are using Erlang >20: ```elixir def deps do [ - {:bcrypt_elixir, "~> 1.1"} + {:bcrypt_elixir, "~> 2.0"} ] end ``` -If you are NOT using Erlang 19 or below: +If you are using Erlang 19 or below: ```elixir def deps do @@ -42,7 +33,7 @@ end ``` 2. Make sure you have a C compiler installed. -See the [Comeonin wiki](https://github.com/riverrun/comeonin/wiki) for details. +See the [Comeonin wiki](https://github.com/riverrun/comeonin/wiki/Requirements) for details. 3. Optional: during tests (and tests only), you may want to reduce the number of rounds so it does not slow down your test suite. If you have a config/test.exs, you should @@ -52,34 +43,16 @@ add: config :bcrypt_elixir, :log_rounds, 4 ``` -## Use - -In most cases, you will just need to use the following three functions: - -* hash_pwd_salt - hash a password with a randomly-generated salt -* verify_pass - check the password by comparing it with a stored hash -* no_user_verify - perform a dummy check to make user enumeration more difficult +## Comeonin wiki -See the documentation for the Bcrypt module for more information. +See the [Comeonin wiki](https://github.com/riverrun/comeonin/wiki) for more +information on the following topics: -For a lower-level api, see the documentation for Bcrypt.Base. - -For further information about password hashing and using Bcrypt with Comeonin, -see the Comeonin [wiki](https://github.com/riverrun/comeonin/wiki). - -### Docker - -In order to use `bcrypt_elixir` in Docker, you will probably need to manually compile it in your Dockerfile. In order to do it on the Alpine image, you're going to need `make`, `gcc` and `libc-dev`. Add the following lines to your Dockerfile, right after `RUN mix deps.get` - -``` -RUN apk add --no-cache make gcc libc-dev -``` - -Remember to add your local `_build` and `deps` folders to `.dockerignore`, because otherwise, you'll see errors coming up. - -### Deployment - -See the Comeonin [deployment guide](https://github.com/riverrun/comeonin/wiki/Deployment). +* [algorithms](https://github.com/riverrun/comeonin/wiki/Choosing-the-password-hashing-algorithm) +* [requirements](https://github.com/riverrun/comeonin/wiki/Requirements) +* [deployment](https://github.com/riverrun/comeonin/wiki/Deployment) + * including information about using Docker +* [references](https://github.com/riverrun/comeonin/wiki/References) ## Contributing @@ -92,8 +65,13 @@ There are many ways you can contribute to the development of this library, inclu ## Donations -If you would like to buy me a cup of coffee, you can -do so through [paypal](https://www.paypal.me/alovedalongthe) +This software is offered free of charge, but if you find it useful +and you would like to buy me a cup of coffee, you can do so through +[paypal](https://www.paypal.me/alovedalongthe). + +### Documentation + +http://hexdocs.pm/bcrypt_elixir ### License diff --git a/lib/bcrypt.ex b/lib/bcrypt.ex index 9229177..b2ae4f4 100644 --- a/lib/bcrypt.ex +++ b/lib/bcrypt.ex @@ -2,12 +2,50 @@ defmodule Bcrypt do @moduledoc """ Bcrypt password hashing library main module. - This library can be used on its own, or it can be used together with - [Comeonin](https://hexdocs.pm/comeonin/api-reference.html), which - provides a higher-level api. + This module implements the Comeonin and Comeonin.PasswordHash behaviours, + providing the following functions: + + * `add_hash/2` - takes a password as input and returns a map containing the password hash + * `check_pass/3` - takes a user struct and password as input and verifies the password + * `no_user_verify/1` - runs the hash function, but always returns false + * `hash_pwd_salt/2` - hashes the password with a randomly-generated salt + * `verify_pass/2` - verifies a password For a lower-level API, see Bcrypt.Base. + ## Configuration + + The following parameter can be set in the config file: + + * `log_rounds` - the computational cost as number of log rounds + * the default is 12 (2^12 rounds) + + If you are hashing passwords in your tests, it can be useful to add + the following to the `config/test.exs` file: + + config :bcrypt_elixir, log_rounds: 4 + + NB. do not use this value in production. + + ## Options + + In addition to the options listed below, the `add_hash`, `no_user_verify` + and `hash_pwd_salt` functions all take a `log_rounds` option, which can be + used to override the value in the config. + + ### add_hash + + * `hash_key` - the key used in the map for the password hash + * the default is `password_hash` + + ### check_pass + + * `hash_key` - the key used in the user struct for the password hash + * if this is not set, `check_pass` will look for `password_hash`, and then `encrypted_password` + * `hide_user` - run `no_user_verify` to prevent user enumeration + * the default is true + * set this to false if you do not want to hide usernames + ## Bcrypt Bcrypt is a key derivation function for passwords designed by Niels Provos @@ -31,8 +69,11 @@ defmodule Bcrypt do The `$2y$` prefix is not supported. For advice on how to use hashes with the `$2y$` prefix, see [this issue](https://github.com/riverrun/comeonin/issues/103). + Hash the password with a salt which is randomly generated. """ + use Comeonin + alias Bcrypt.Base @doc """ @@ -47,39 +88,10 @@ defmodule Bcrypt do by older libraries. """ def gen_salt(log_rounds \\ 12, legacy \\ false) do - :crypto.strong_rand_bytes(16) - |> Base.gensalt_nif(log_rounds, (legacy and 97) || 98) + Base.gensalt_nif(:crypto.strong_rand_bytes(16), log_rounds, (legacy and 97) || 98) end - @doc """ - Hash the password with a salt which is randomly generated. - - ## Configurable parameters - - The following parameters can be set in the config file: - - * `log_rounds` - the computational cost as number of log rounds, by default - it is 12 (2^12). - - If you are hashing passwords in your tests, it can be useful to add - the following to the `config/test.exs` file: - - config :bcrypt_elixir, - log_rounds: 4 - - NB. do not use this value in production. - - ## Options - - There is one option (this can be used if you want to override the - value in the config): - - * `:log_rounds` - override the application's configured computational cost. - * `:legacy` - whether to generate a salt with the old `$2a$` prefix. This - should only be used to generate hashes that will be checked by older - libraries. - - """ + @impl true def hash_pwd_salt(password, opts \\ []) do Base.hash_password( password, @@ -90,28 +102,12 @@ defmodule Bcrypt do ) end - @doc """ - Check the password. - - The check is performed in constant time to avoid timing attacks. - """ + @impl true def verify_pass(password, stored_hash) do Base.checkpass_nif(:binary.bin_to_list(password), :binary.bin_to_list(stored_hash)) |> handle_verify end - @doc """ - A dummy verify function to help prevent user enumeration. - - This always returns false. The reason for implementing this check is - in order to make it more difficult for an attacker to identify users - by timing responses. - """ - def no_user_verify(opts \\ []) do - hash_pwd_salt("password", opts) - false - end - defp handle_verify(0), do: true defp handle_verify(_), do: false end diff --git a/lib/bcrypt/stats.ex b/lib/bcrypt/stats.ex index 95919af..3f64fa9 100644 --- a/lib/bcrypt/stats.ex +++ b/lib/bcrypt/stats.ex @@ -2,7 +2,10 @@ defmodule Bcrypt.Stats do @moduledoc """ Module to provide statistics for the Bcrypt password hashing function. - ## Configuring Bcrypt + The `report` function in this module can be used to help you configure + Bcrypt. + + ## Configuration There is one configuration option for Bcrypt - log_rounds. Increasing this value will increase the complexity, and time diff --git a/mix.exs b/mix.exs index d3081e8..5dbf54c 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule BcryptElixir.Mixfile do use Mix.Project - @version "1.1.1" + @version "2.0.0" @description """ Bcrypt password hashing algorithm for Elixir @@ -11,7 +11,7 @@ defmodule BcryptElixir.Mixfile do [ app: :bcrypt_elixir, version: @version, - elixir: "~> 1.4", + elixir: "~> 1.7", start_permanent: Mix.env() == :prod, compilers: [:elixir_make] ++ Mix.compilers(), description: @description, @@ -29,6 +29,7 @@ defmodule BcryptElixir.Mixfile do defp deps do [ + {:comeonin, "~> 5.0"}, {:elixir_make, "~> 0.4", runtime: false}, {:ex_doc, "~> 0.19", only: :dev, runtime: false} ] diff --git a/mix.lock b/mix.lock index 5a57b3a..3e8ea94 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "comeonin": {:git, "https://github.com/riverrun/comeonin.git", "47cd3ce2bbbc540d1d84d1ae0ba4e73e2373b30d", [branch: "v5.0"]}, "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/bcrypt_test.exs b/test/bcrypt_test.exs index 970e1c1..821f5c0 100644 --- a/test/bcrypt_test.exs +++ b/test/bcrypt_test.exs @@ -1,45 +1,42 @@ defmodule BcryptTest do use ExUnit.Case - def hash_check_password(password, wrong1, wrong2, wrong3) do - hash = Bcrypt.hash_pwd_salt(password) - assert Bcrypt.verify_pass(password, hash) == true - assert Bcrypt.verify_pass(wrong1, hash) == false - assert Bcrypt.verify_pass(wrong2, hash) == false - assert Bcrypt.verify_pass(wrong3, hash) == false - end + import BcryptTestHelper test "hashing and checking passwords" do - hash_check_password("password", "passwor", "passwords", "pasword") - hash_check_password("hard2guess", "ha rd2guess", "had2guess", "hardtoguess") + wrong_list = ["aged2h$ru", "2dau$ehgr", "rg$deh2au", "2edrah$gu", "$agedhur2", ""] + password_hash_check("hard2guess", wrong_list) end test "hashing and checking passwords with characters from the extended ascii set" do - hash_check_password("aáåäeéêëoôö", "aáåäeéêëoö", "aáåeéêëoôö", "aáå äeéêëoôö") - hash_check_password("aáåä eéêëoôö", "aáåä eéê ëoö", "a áåeé êëoôö", "aáå äeéêëoôö") + wrong_list = ["eáé åöêô ëaäo", "aäôáö eéoêë å", " aöêôée oåäëá", "åaêöéäëeoô á ", ""] + password_hash_check("aáåä eéê ëoôö", wrong_list) end test "hashing and checking passwords with non-ascii characters" do - hash_check_password( - "Сколько лет, сколько зим", - "Сколько лет,сколько зим", - "Сколько лет сколько зим", - "Сколько лет, сколько" - ) + wrong_list = [ + "и Скл;лекьоток к олсомзь", + "кеокок зС омлслтььлок;и", + "е о оиькльлтСо;осккклзм", + "" + ] - hash_check_password("สวัสดีครับ", "สวัดีครับ", "สวัสสดีครับ", "วัสดีครับ") + password_hash_check("Сколько лет; сколько зим", wrong_list) end test "hashing and checking passwords with mixed characters" do - hash_check_password("Я❤três☕ où☔", "Я❤tres☕ où☔", "Я❤três☕où☔", "Я❤três où☔") + wrong_list = ["Я☕t☔s❤ùo", "o❤ Я☔ùrtês☕", " ùt❤o☕☔srêЯ", "ù☕os êt❤☔rЯ", ""] + password_hash_check("Я❤três☕ où☔", wrong_list) + end + + test "check password using check_pass, which uses the user map as input" do + wrong_list = ["บดสคสััีวร", "สดรบัีสัคว", "สวดัรคบัสี", "ดรสสีวคบัั", "วรคดสัสีับ", ""] + check_pass_check("สวัสดีครับ", wrong_list) end - test "hash_pwd_salt number of rounds" do - assert String.starts_with?(Bcrypt.hash_pwd_salt("", log_rounds: 4), "$2b$04$") - Application.put_env(:bcrypt_elixir, :log_rounds, 4) - assert String.starts_with?(Bcrypt.hash_pwd_salt(""), "$2b$04$") - Application.delete_env(:bcrypt_elixir, :log_rounds) - assert String.starts_with?(Bcrypt.hash_pwd_salt(""), "$2b$12$") + test "add hash to map and set password to nil" do + wrong_list = ["êäöéaoeôáåë", "åáoêëäéôeaö", "aäáeåëéöêôo", ""] + add_hash_check("aáåäeéêëoôö", wrong_list) end test "hash_pwd_salt legacy prefix" do @@ -68,4 +65,41 @@ defmodule BcryptTest do assert String.starts_with?(Bcrypt.gen_salt(8, true), "$2a$08$") assert String.starts_with?(Bcrypt.gen_salt(12, true), "$2a$12$") end + + test "add_hash and check_pass" do + assert {:ok, user} = Bcrypt.add_hash("password") |> Bcrypt.check_pass("password") + assert {:error, "invalid password"} = + Bcrypt.add_hash("pass") |> Bcrypt.check_pass("password") + assert Map.has_key?(user, :password_hash) + end + + test "add_hash with a custom hash_key and check_pass" do + assert {:ok, user} = + Bcrypt.add_hash("password", hash_key: :encrypted_password) + |> Bcrypt.check_pass("password") + assert {:error, "invalid password"} = + Bcrypt.add_hash("pass", hash_key: :encrypted_password) + |> Bcrypt.check_pass("password") + assert Map.has_key?(user, :encrypted_password) + end + + test "check_pass with custom hash_key" do + assert {:ok, user} = + Bcrypt.add_hash("password", hash_key: :custom_hash) + |> Bcrypt.check_pass("password", hash_key: :custom_hash) + assert Map.has_key?(user, :custom_hash) + end + + test "check_pass with invalid hash_key" do + {:error, message} = + Bcrypt.add_hash("password", hash_key: :unconventional_name) + |> Bcrypt.check_pass("password") + + assert message =~ "no password hash found" + end + + test "check_pass with password that is not a string" do + assert {:error, message} = Bcrypt.add_hash("pass") |> Bcrypt.check_pass(nil) + assert message =~ "password is not a string" + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..0d2c6f1 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,34 @@ ExUnit.start() + +defmodule BcryptTestHelper do + use ExUnit.Case + + def password_hash_check(password, wrong_list) do + hash = Bcrypt.hash_pwd_salt(password) + assert Bcrypt.verify_pass(password, hash) + + for wrong <- wrong_list do + refute Bcrypt.verify_pass(wrong, hash) + end + end + + def add_hash_check(password, wrong_list) do + %{password_hash: hash, password: nil} = Bcrypt.add_hash(password) + assert Bcrypt.verify_pass(password, hash) + + for wrong <- wrong_list do + refute Bcrypt.verify_pass(wrong, hash) + end + end + + def check_pass_check(password, wrong_list) do + hash = Bcrypt.hash_pwd_salt(password) + user = %{id: 2, name: "fred", password_hash: hash} + assert Bcrypt.check_pass(user, password) == {:ok, user} + assert Bcrypt.check_pass(nil, password) == {:error, "invalid user-identifier"} + + for wrong <- wrong_list do + assert Bcrypt.check_pass(user, wrong) == {:error, "invalid password"} + end + end +end