├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── lib ├── pbkdf2.ex └── pbkdf2 │ ├── base.ex │ ├── base64.ex │ ├── stats.ex │ └── tools.ex ├── mix.exs ├── mix.lock └── test ├── base64_test.exs ├── base_test.exs ├── pbkdf2_test.exs ├── reference_test.exs ├── stats_test.exs ├── support ├── pbkdf2_sha256_test_vectors └── pbkdf2_sha512_test_vectors └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.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 third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | pbkdf2_elixir-*.tar 24 | 25 | # Temporary files for e.g. tests 26 | /tmp/ 27 | 28 | # Dialyzer plts files 29 | /priv/ 30 | -------------------------------------------------------------------------------- /.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 15 | 16 | cache: 17 | directories: 18 | - priv/plts 19 | 20 | sudo: false 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## v2.3.0 (2024-10-04) 9 | 10 | * Changes 11 | * Updated dependencies and made changes to silence warnings in Elixir 1.17 12 | 13 | ## v2.2.0 - 2023-08-26 14 | 15 | * Changes 16 | * Updated dependencies and documentation (through updates to the Comeonin documentation) 17 | 18 | ## v2.0.0 - 2022-01-20 19 | 20 | * Changes 21 | * updated `gen_salt` and moved it to the `Base` module 22 | 23 | ## v1.4.1 - 2022-01-19 24 | 25 | * Changes 26 | * updated documentation and README 27 | 28 | ## v1.4.0 - 2021-04-08 29 | 30 | * Changes 31 | * updated hmac sha function to support the new crypto api in OTP 24 32 | 33 | ## v1.3.0 - 2021-01-08 34 | 35 | * Bug fixes 36 | * made sure that `hash_pwd_salt/2` passes the `format: :django` option onto `gen_salt/1` 37 | * Changes 38 | * changed minimum salt length to 0 bytes and added warning for salts between 0 and 8 bytes long 39 | * updated documentation about salt length with more information about the minimum recommended value 40 | * updated `gen_salt/1` to take a keyword list by default (an integer is also allowed for backwards compatibility) 41 | * Deprecations 42 | * `Base.django_salt/1` has been deprecated - `gen_salt/1` can be used instead 43 | 44 | ## v1.2.0 - 2020-03-01 45 | 46 | * Changes 47 | * using Comeonin v5.3, which changes `add_hash/2` so that it does NOT set the password to nil 48 | 49 | ## v1.1.0 - 2020-01-20 50 | 51 | * Enhancements 52 | * Updated documentation - in line with updates to Comeonin v5.2 53 | 54 | ## v1.0.0 - 2019-02-19 55 | 56 | * Enhancements 57 | * Updated to use Comeonin behaviour 58 | 59 | ## v0.12.0 - 2017-07-21 60 | 61 | * Changes 62 | * Created separate Pbkdf2 library 63 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All code in this application, unless otherwise stated, is subject 2 | to the following license: 3 | 4 | Copyright (c) 2014-2021 David Whitlock (alovedalongthe@gmail.com) 5 | 6 | Some rights reserved. 7 | Redistribution and use in source and binary forms of the software as well 8 | as documentation, with or without modification, are permitted provided 9 | that the 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 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 22 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 23 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 25 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 26 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 27 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 32 | DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pbkdf2 2 | 3 | [![Build Status](https://travis-ci.com/riverrun/pbkdf2_elixir.svg?branch=master)](https://travis-ci.com/riverrun/pbkdf2_elixir) 4 | [![Module Version](https://img.shields.io/hexpm/v/pbkdf2_elixir.svg)](https://hex.pm/packages/pbkdf2_elixir) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/pbkdf2_elixir/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/pbkdf2_elixir.svg)](https://hex.pm/packages/pbkdf2_elixir) 7 | [![License](https://img.shields.io/hexpm/l/pbkdf2_elixir.svg)](https://github.com/riverrun/pbkdf2_elixir/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/riverrun/pbkdf2_elixir.svg)](https://github.com/riverrun/pbkdf2_elixir/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 | Pbkdf2 password hashing library for Elixir. 12 | 13 | Pbkdf2 is a well-tested password-based key derivation function that can be 14 | configured to remain slow and resistant to brute-force attacks even as 15 | computational power increases. 16 | 17 | ## Comparison with the Plug.Crypto version of Pbkdf2 18 | 19 | If you want the Pbkdf2 output to be in binary (raw) or hex format, you might 20 | find [Plug.Crypto.KeyGenerator](https://hexdocs.pm/plug_crypto/Plug.Crypto.KeyGenerator.html) 21 | more convenient. 22 | 23 | ## Installation 24 | 25 | 1. Add `:pbkdf2_elixir` to the `deps` section of your `mix.exs` file: 26 | 27 | ```elixir 28 | def deps do 29 | [ 30 | {:pbkdf2_elixir, "~> 2.0"} 31 | ] 32 | end 33 | ``` 34 | 35 | 2. Optional: during tests (and tests only), you may want to reduce the number of rounds 36 | so it does not slow down your test suite. If you have a `config/test.exs`, you should 37 | add: 38 | 39 | ```elixir 40 | config :pbkdf2_elixir, :rounds, 1 41 | ``` 42 | 43 | ## Comeonin wiki 44 | 45 | See the [Comeonin wiki](https://github.com/riverrun/comeonin/wiki) for more 46 | information on the following topics: 47 | 48 | * [Algorithms](https://github.com/riverrun/comeonin/wiki/Choosing-the-password-hashing-algorithm) 49 | * [Requirements](https://github.com/riverrun/comeonin/wiki/Requirements) 50 | * [Deployment](https://github.com/riverrun/comeonin/wiki/Deployment) 51 | * including information about using Docker 52 | * [References](https://github.com/riverrun/comeonin/wiki/References) 53 | 54 | ## Contributing 55 | 56 | There are many ways you can contribute to the development of this library, including: 57 | 58 | * Reporting issues 59 | * Improving documentation 60 | * Sharing your experiences with others 61 | 62 | ## Copyright and License 63 | 64 | Copyright (c) 2014-2021 David Whitlock (alovedalongthe@gmail.com) 65 | 66 | This software is licensed under [the BSD-3-Clause license](./LICENSE.md). 67 | -------------------------------------------------------------------------------- /lib/pbkdf2.ex: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2 do 2 | @moduledoc """ 3 | Elixir wrapper for the Pbkdf2 password hashing function. 4 | 5 | For a lower-level API, see `Pbkdf2.Base`. 6 | 7 | ## Configuration 8 | 9 | The following parameter can be set in the config file: 10 | 11 | * `:rounds` - computational cost 12 | * the number of rounds 13 | * `160_000` is the default 14 | 15 | If you are hashing passwords in your tests, it can be useful to add 16 | the following to the `config/test.exs` file: 17 | 18 | # Note: Do not use this value in production 19 | config :pbkdf2_elixir, 20 | rounds: 1 21 | 22 | ## Pbkdf2 23 | 24 | Pbkdf2 is a password-based key derivation function 25 | that uses a password, a variable-length salt and an iteration 26 | count and applies a pseudorandom function to these to 27 | produce a key. 28 | 29 | The original implementation used SHA-1 as the pseudorandom function, 30 | but this version uses HMAC-SHA-512, the default, or HMAC-SHA-256. 31 | 32 | ## Warning 33 | 34 | It is recommended that you set a maximum length for the password 35 | when using Pbkdf2. This maximum length should not prevent valid users from setting 36 | long passwords. It is instead needed to combat denial-of-service attacks. 37 | As an example, Django sets the maximum length to `4096` bytes. 38 | For more information, see [this link](https://www.djangoproject.com/weblog/2013/sep/15/security/). 39 | """ 40 | 41 | use Comeonin 42 | 43 | alias Pbkdf2.Base 44 | 45 | @doc """ 46 | Hashes a password with a randomly generated salt. 47 | 48 | ## Options 49 | 50 | In addition to the options for `Pbkdf2.Base.gen_salt/1` (`:salt_len` and 51 | `:format`), this function also takes options that are then passed on to 52 | the `hash_password` function in the `Pbkdf2.Base` module. 53 | 54 | See the documentation for `Pbkdf2.Base.hash_password/3` for further details. 55 | 56 | ## Examples 57 | 58 | The following examples show how to hash a password with a randomly-generated 59 | salt and then verify a password: 60 | 61 | iex> hash = Pbkdf2.hash_pwd_salt("password") 62 | ...> Pbkdf2.verify_pass("password", hash) 63 | true 64 | 65 | iex> hash = Pbkdf2.hash_pwd_salt("password") 66 | ...> Pbkdf2.verify_pass("incorrect", hash) 67 | false 68 | 69 | The next examples show how to use some of the various available options: 70 | 71 | iex> hash = Pbkdf2.hash_pwd_salt("password", rounds: 100_000) 72 | ...> Pbkdf2.verify_pass("password", hash) 73 | true 74 | 75 | iex> hash = Pbkdf2.hash_pwd_salt("password", digest: :sha256) 76 | ...> Pbkdf2.verify_pass("password", hash) 77 | true 78 | 79 | iex> hash = Pbkdf2.hash_pwd_salt("password", digest: :sha256, format: :django) 80 | ...> Pbkdf2.verify_pass("password", hash) 81 | true 82 | 83 | """ 84 | @impl true 85 | def hash_pwd_salt(password, opts \\ []) do 86 | Base.hash_password(password, Base.gen_salt(opts), opts) 87 | end 88 | 89 | @doc """ 90 | Verifies a password by hashing the password and comparing the hashed value 91 | with a stored hash. 92 | 93 | See the documentation for `hash_pwd_salt/2` for examples of using this function. 94 | """ 95 | @impl true 96 | def verify_pass(password, stored_hash) do 97 | [alg, rounds, salt, hash] = String.split(stored_hash, "$", trim: true) 98 | digest = if alg =~ "sha512", do: :sha512, else: :sha256 99 | Base.verify_pass(password, hash, salt, digest, rounds, output(stored_hash)) 100 | end 101 | 102 | defp output("$pbkdf2" <> _), do: :modular 103 | defp output("pbkdf2" <> _), do: :django 104 | end 105 | -------------------------------------------------------------------------------- /lib/pbkdf2/base.ex: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.Base do 2 | @moduledoc """ 3 | Base module for the Pbkdf2 password hashing library. 4 | """ 5 | 6 | import Bitwise 7 | 8 | alias Pbkdf2.{Base64, Tools} 9 | 10 | @max_length bsl(1, 32) - 1 11 | 12 | @doc """ 13 | Generates a random salt. 14 | 15 | This function takes one optional argument - a keyword list (see below 16 | for more details). 17 | 18 | ## Options 19 | 20 | The following options are available: 21 | 22 | * `:salt_len` - the length of the random salt 23 | * the default is 16 bytes 24 | * for more information, see the 'Salt length recommendations' section below 25 | * `:format` - the length of the random salt 26 | * the default is `:modular` (modular crypt format) 27 | * the other available options are `:django` and `:hex` 28 | 29 | ## Examples 30 | 31 | Here is an example of generating a salt with the default salt length and format: 32 | 33 | Pbkdf2.Base.gen_salt() 34 | 35 | To generate a different length salt: 36 | 37 | Pbkdf2.Base.gen_salt(salt_len: 32) 38 | 39 | And to generate a salt in Django output format: 40 | 41 | Pbkdf2.Base.gen_salt(format: :django) 42 | 43 | ## Salt length recommendations 44 | 45 | In most cases, 16 bytes is a suitable length for the salt. 46 | It is not recommended to use a salt that is shorter than this 47 | (see below for details and references). 48 | 49 | According to the [Pbkdf2 standard](https://tools.ietf.org/html/rfc8018), 50 | the salt should be at least 8 bytes long, but according to [NIST 51 | recommendations](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf), 52 | the minimum salt length should be 16 bytes. 53 | """ 54 | @spec gen_salt(keyword | integer) :: binary 55 | def gen_salt(opts \\ []) 56 | 57 | def gen_salt(salt_len) when is_integer(salt_len) do 58 | gen_salt(salt_len: salt_len) 59 | end 60 | 61 | def gen_salt(opts) do 62 | salt_len = Keyword.get(opts, :salt_len, 16) 63 | Tools.check_salt_length(salt_len) 64 | 65 | case opts[:format] do 66 | :django -> Tools.get_random_string(salt_len) 67 | _ -> :crypto.strong_rand_bytes(salt_len) 68 | end 69 | end 70 | 71 | @doc """ 72 | Hash a password using Pbkdf2. 73 | 74 | ## Options 75 | 76 | There are four options (`rounds` can be used to override the value 77 | in the config): 78 | 79 | * `:rounds` - the number of rounds 80 | * the amount of computation, given in number of iterations 81 | * the default is 160_000 82 | * this can also be set in the config file 83 | * `:format` - the output format of the hash 84 | * the default is `:modular` - modular crypt format 85 | * the other available formats are: 86 | * `:django` - the format used in django applications 87 | * `:hex` - the hash is encoded in hexadecimal 88 | * `:digest` - the sha algorithm that pbkdf2 will use 89 | * the default is sha512 90 | * `:length` - the length, in bytes, of the hash 91 | * the default is 64 for sha512 and 32 for sha256 92 | 93 | """ 94 | @spec hash_password(binary, binary, keyword) :: binary 95 | def hash_password(password, salt, opts \\ []) do 96 | Tools.check_salt_length(byte_size(salt)) 97 | {rounds, output_fmt, {digest, length}} = get_opts(opts) 98 | 99 | if length > @max_length do 100 | raise ArgumentError, "length must be equal to or less than #{@max_length}" 101 | end 102 | 103 | password 104 | |> create_hash(salt, digest, rounds, length) 105 | |> format(salt, digest, rounds, output_fmt) 106 | end 107 | 108 | @doc """ 109 | Verify a password by comparing it with the stored Pbkdf2 hash. 110 | """ 111 | @spec verify_pass(binary, binary, binary, atom, binary, atom) :: boolean 112 | def verify_pass(password, hash, salt, digest, rounds, output_fmt) do 113 | {salt, length} = 114 | case output_fmt do 115 | :modular -> {Base64.decode(salt), byte_size(Base64.decode(hash))} 116 | :django -> {salt, byte_size(Base.decode64!(hash))} 117 | :hex -> {salt, byte_size(Base.decode16!(hash, case: :lower))} 118 | end 119 | 120 | password 121 | |> create_hash(salt, digest, String.to_integer(rounds), length) 122 | |> encode(output_fmt) 123 | |> Tools.secure_check(hash) 124 | end 125 | 126 | defp get_opts(opts) do 127 | { 128 | Keyword.get(opts, :rounds, Application.get_env(:pbkdf2_elixir, :rounds, 160_000)), 129 | Keyword.get(opts, :format, :modular), 130 | case opts[:digest] do 131 | :sha256 -> {:sha256, opts[:length] || 32} 132 | _ -> {:sha512, opts[:length] || 64} 133 | end 134 | } 135 | end 136 | 137 | defp create_hash(password, salt, digest, rounds, length) do 138 | digest 139 | |> hmac_fun(password) 140 | |> do_create_hash(salt, rounds, length, 1, [], 0) 141 | end 142 | 143 | defp do_create_hash(_fun, _salt, _rounds, dklen, _block_index, acc, length) 144 | when length >= dklen do 145 | key = acc |> Enum.reverse() |> IO.iodata_to_binary() 146 | <> = key 147 | bin 148 | end 149 | 150 | defp do_create_hash(fun, salt, rounds, dklen, block_index, acc, length) do 151 | initial = fun.(<>) 152 | block = iterate(fun, rounds - 1, initial, initial) 153 | 154 | do_create_hash( 155 | fun, 156 | salt, 157 | rounds, 158 | dklen, 159 | block_index + 1, 160 | [block | acc], 161 | byte_size(block) + length 162 | ) 163 | end 164 | 165 | defp iterate(_fun, 0, _prev, acc), do: acc 166 | 167 | defp iterate(fun, round, prev, acc) do 168 | next = fun.(prev) 169 | iterate(fun, round - 1, next, :crypto.exor(next, acc)) 170 | end 171 | 172 | defp format(hash, salt, digest, rounds, :modular) do 173 | "$pbkdf2-#{digest}$#{rounds}$#{Base64.encode(salt)}$#{Base64.encode(hash)}" 174 | end 175 | 176 | defp format(hash, salt, digest, rounds, :django) do 177 | "pbkdf2_#{digest}$#{rounds}$#{salt}$#{Base.encode64(hash)}" 178 | end 179 | 180 | defp format(hash, _salt, _digest, _rounds, :hex), do: Base.encode16(hash, case: :lower) 181 | 182 | defp encode(hash, :modular), do: Base64.encode(hash) 183 | defp encode(hash, :django), do: Base.encode64(hash) 184 | defp encode(hash, :hex), do: Base.encode16(hash, case: :lower) 185 | 186 | if System.otp_release() >= "22" do 187 | defp hmac_fun(digest, key), do: &:crypto.mac(:hmac, digest, key, &1) 188 | else 189 | defp hmac_fun(digest, key), do: &:crypto.hmac(digest, key, &1) 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/pbkdf2/base64.ex: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.Base64 do 2 | @moduledoc """ 3 | Module that provides base64 encoding for Pbkdf2. 4 | 5 | Most developers will not need to use this module directly. 6 | 7 | Pbkdf2 uses an adapted base64 alphabet (using `.` instead of `+` 8 | and with no padding). 9 | """ 10 | 11 | import Bitwise 12 | 13 | b64_alphabet = 14 | Enum.with_index(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./") 15 | 16 | for {encoding, value} <- b64_alphabet do 17 | defp unquote(:enc64)(unquote(value)), do: unquote(encoding) 18 | defp unquote(:dec64)(unquote(encoding)), do: unquote(value) 19 | end 20 | 21 | @doc """ 22 | Encode using the adapted Pbkdf2 alphabet. 23 | 24 | ## Examples 25 | 26 | iex> Pbkdf2.Base64.encode("spamandeggs") 27 | "c3BhbWFuZGVnZ3M" 28 | 29 | """ 30 | def encode(<<>>), do: <<>> 31 | 32 | def encode(data) do 33 | split = 3 * div(byte_size(data), 3) 34 | <> = data 35 | main = for <>, into: <<>>, do: <> 36 | 37 | case rest do 38 | <> -> 39 | <> 40 | 41 | <> -> 42 | <> 43 | 44 | <<>> -> 45 | main 46 | end 47 | end 48 | 49 | @doc """ 50 | Decode using the adapted Pbkdf2 alphabet. 51 | 52 | ## Examples 53 | 54 | iex> Pbkdf2.Base64.decode("c3BhbWFuZGVnZ3M") 55 | "spamandeggs" 56 | 57 | """ 58 | def decode(<<>>), do: <<>> 59 | 60 | def decode(data) do 61 | split = 4 * div(byte_size(data), 4) 62 | <> = data 63 | main = for <>, into: <<>>, do: <> 64 | 65 | case rest do 66 | <> -> 67 | <> 68 | 69 | <> -> 70 | <> 71 | 72 | <<>> -> 73 | main 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/pbkdf2/stats.ex: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.Stats do 2 | @moduledoc """ 3 | Module to provide statistics for the Pbkdf2 password hashing function. 4 | 5 | ## Configuring Pbkdf2 6 | 7 | The main configuration option for Pbkdf2 is the number of rounds that 8 | it uses. Increasing this value will increase the complexity, and time 9 | taken, of the Pbkdf2 function. 10 | 11 | Increasing the time that a password hash function takes makes it more 12 | difficult for an attacker to find the correct password. However, the 13 | amount of time a valid user has to wait also needs to be taken into 14 | consideration when setting the number of rounds. 15 | 16 | The correct number of rounds depends on circumstances specific to your 17 | use case, such as what level of security you want, how often the user 18 | has to log in, and the hardware you are using. However, for password 19 | hashing, we do not recommend setting the number of rounds to anything 20 | less than 100_000. 21 | """ 22 | 23 | alias Pbkdf2.Base64 24 | 25 | @doc """ 26 | Hash a password with Pbkdf2 and print out a report. 27 | 28 | This function hashes a password, and salt, with Pbkdf2.Base.hash_password/3 29 | and prints out statistics which can help you choose how to configure Pbkdf2. 30 | 31 | ## Options 32 | 33 | In addition to the options for Pbkdf2.Base.hash_password (rounds, output_fmt, 34 | digest and length), there are two options: 35 | 36 | * `:password` - the password used 37 | * the default is "password" 38 | * `:salt` - the salt used 39 | * the default is "somesaltSOMESALT" 40 | 41 | """ 42 | def report(opts \\ []) do 43 | password = Keyword.get(opts, :password, "password") 44 | salt = Keyword.get(opts, :salt, "somesaltSOMESALT") 45 | {exec_time, encoded} = :timer.tc(Pbkdf2.Base, :hash_password, [password, salt, opts]) 46 | 47 | Pbkdf2.verify_pass(password, encoded) 48 | |> format_result(encoded, exec_time) 49 | end 50 | 51 | defp format_result(check, encoded, exec_time) do 52 | [alg, rounds, _, hash] = String.split(encoded, "$", trim: true) 53 | 54 | IO.puts(""" 55 | Digest:\t\t#{alg} 56 | Digest length:\t#{digest_length(encoded, hash)} 57 | Hash:\t\t#{encoded} 58 | Rounds:\t\t#{rounds} 59 | Time taken:\t#{format_time(exec_time)} seconds 60 | Verification #{if check, do: "OK", else: "FAILED"} 61 | """) 62 | end 63 | 64 | defp digest_length("$pbkdf2" <> _, hash), do: Base64.decode(hash) |> byte_size 65 | defp digest_length("pbkdf2" <> _, hash), do: Base.decode64!(hash) |> byte_size 66 | 67 | defp format_time(time) do 68 | Float.round(time / 1_000_000, 2) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/pbkdf2/tools.ex: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.Tools do 2 | @moduledoc false 3 | 4 | import Bitwise 5 | 6 | @allowed_chars ~c"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 7 | 8 | def get_random_string(len, allowed_chars \\ @allowed_chars) do 9 | :crypto.rand_seed() 10 | high = length(allowed_chars) 11 | 12 | Enum.to_list(1..len) 13 | |> Enum.reduce([], fn _x, acc -> 14 | [Enum.at(allowed_chars, :rand.uniform(high) - 1)] ++ acc 15 | end) 16 | |> to_string() 17 | end 18 | 19 | def check_salt_length(salt_len) when salt_len < 8 do 20 | IO.warn( 21 | "Using a salt less than 8 bytes long is not recommended. " <> 22 | "Please see the documentation for details." 23 | ) 24 | 25 | :ok 26 | end 27 | 28 | def check_salt_length(salt_len) when salt_len > 1024 do 29 | raise ArgumentError, """ 30 | The salt is too long. The maximum length is 1024 bytes. 31 | """ 32 | end 33 | 34 | def check_salt_length(_), do: :ok 35 | 36 | def secure_check(hash, stored) do 37 | if byte_size(hash) == byte_size(stored) do 38 | secure_check(hash, stored, 0) == 0 39 | else 40 | false 41 | end 42 | end 43 | 44 | defp secure_check(<>, <>, acc) do 45 | secure_check(rest_h, rest_s, acc ||| bxor(h, s)) 46 | end 47 | 48 | defp secure_check("", "", acc) do 49 | acc 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2Elixir.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/riverrun/pbkdf2_elixir" 5 | @version "2.3.1" 6 | 7 | def project do 8 | [ 9 | app: :pbkdf2_elixir, 10 | version: @version, 11 | elixir: "~> 1.7", 12 | start_permanent: Mix.env() == :prod, 13 | package: package(), 14 | deps: deps(), 15 | docs: docs(), 16 | dialyzer: [ 17 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"} 18 | ] 19 | ] 20 | end 21 | 22 | def application do 23 | [ 24 | extra_applications: [:logger, :crypto] 25 | ] 26 | end 27 | 28 | defp deps do 29 | [ 30 | {:comeonin, "~> 5.3"}, 31 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 32 | {:dialyxir, "~> 1.3", only: :dev, runtime: false} 33 | ] 34 | end 35 | 36 | defp package do 37 | [ 38 | description: "Pbkdf2 password hashing algorithm for Elixir.", 39 | files: ["lib", "mix.exs", "README.md", "LICENSE.md", "CHANGELOG.md"], 40 | maintainers: ["David Whitlock"], 41 | licenses: ["BSD-3-Clause"], 42 | links: %{ 43 | "Changelog" => "https://hexdocs.pm/pbkdf2_elixir/changelog.html", 44 | "GitHub" => @source_url 45 | } 46 | ] 47 | end 48 | 49 | defp docs do 50 | [ 51 | extras: [ 52 | "CHANGELOG.md", 53 | {:"LICENSE.md", [title: "License"]}, 54 | "README.md" 55 | ], 56 | main: "readme", 57 | source_url: @source_url, 58 | source_ref: "v#{@version}", 59 | formatters: ["html"], 60 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"] 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, 3 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 4 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 8 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 10 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 12 | } 13 | -------------------------------------------------------------------------------- /test/base64_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.Base64Test do 2 | use ExUnit.Case 3 | doctest Pbkdf2.Base64 4 | end 5 | -------------------------------------------------------------------------------- /test/base_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.BaseTest do 2 | use ExUnit.Case, async: false 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias Pbkdf2.Base 7 | 8 | def check_vectors(data, digest \\ :sha512, format \\ :modular) do 9 | for {password, salt, rounds, stored_hash} <- data do 10 | opts = [rounds: rounds, digest: digest, format: format] 11 | assert Base.hash_password(password, salt, opts) == stored_hash 12 | [_, _, encoded_salt, hash] = String.split(stored_hash, "$", trim: true) 13 | assert Base.verify_pass(password, hash, encoded_salt, digest, to_string(rounds), format) 14 | assert Pbkdf2.verify_pass(password, stored_hash) 15 | end 16 | end 17 | 18 | test "gen_salt length of salt" do 19 | assert byte_size(Base.gen_salt()) == 16 20 | assert byte_size(Base.gen_salt(salt_len: 32)) == 32 21 | assert byte_size(Base.gen_salt(salt_len: 64)) == 64 22 | end 23 | 24 | test "gen_salt with `format: :django` returns django format salt" do 25 | assert byte_size(Base.gen_salt(format: :django)) == 16 26 | assert String.match?(Base.gen_salt(format: :django), ~r/^[A-Za-z0-9+$_=\/]*$/) 27 | end 28 | 29 | test "gen_salt run with an integer creates the correct length salt" do 30 | assert byte_size(Base.gen_salt(32)) == 32 31 | assert byte_size(Base.gen_salt(64)) == 64 32 | end 33 | 34 | test "gen_salt prints warnings for salts that are too short" do 35 | assert capture_io(:stderr, fn -> Base.gen_salt(7) end) =~ 36 | "salt less than 8 bytes long is not recommended" 37 | end 38 | 39 | test "gen_salt raises if salt is too long" do 40 | assert_raise ArgumentError, fn -> 41 | Base.gen_salt(1025) 42 | end 43 | end 44 | 45 | test "base pbkdf2_sha512 tests" do 46 | [ 47 | { 48 | "passDATAb00AB7YxDTT", 49 | "saltKEYbcTcXHCBxtjD", 50 | 100_000, 51 | "$pbkdf2-sha512$100000$c2FsdEtFWWJjVGNYSENCeHRqRA$rM3Nh5iuXNhYBHOQFe8qEeMlkbe30W92gZswsNSdgOGr6myYIrgKH9/kIeJvVgPsqKR6ZMmgBPta.CKfdi/0Hw" 52 | }, 53 | { 54 | "passDATAb00AB7YxDTTl", 55 | "saltKEYbcTcXHCBxtjD2", 56 | 100_000, 57 | "$pbkdf2-sha512$100000$c2FsdEtFWWJjVGNYSENCeHRqRDI$WUJWsL1NbJ8hqH97pXcqeRoQ5hEGlPRDZc2UZw5X8a7NeX7x0QAZOHGQRMfwGAJml4Reua2X2X3jarh4aqtQlg" 58 | }, 59 | { 60 | "passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE5", 61 | "saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJe", 62 | 100_000, 63 | "$pbkdf2-sha512$100000$c2FsdEtFWWJjVGNYSENCeHRqRDJQbkJoNDRBSVE2WFVPQ0VTT2hYcEVwM0hyY0dNd2JqelFLTVNhZjYzSUpl$B0R0AchXZuSu1YPeLmv1pnXqvk82GCgclWFvT8H9/m7LwcOYJ4nU/ZQdZYTvU0p4vTeuAlVdlFXo8In9tN.2uw" 64 | } 65 | ] 66 | |> check_vectors 67 | end 68 | 69 | test "Python passlib pbkdf2_sha512 tests" do 70 | [ 71 | { 72 | "password", 73 | <<36, 196, 248, 159, 51, 166, 84, 170, 213, 250, 159, 211, 154, 83, 10, 193>>, 74 | 19_000, 75 | "$pbkdf2-sha512$19000$JMT4nzOmVKrV.p/TmlMKwQ$jKbZHoPwUWBT08pjb/CnUZmFcB9JW4dsOzVkfi9X6Pdn5NXWeY.mhL1Bm4V9rjYL5ZfA32uh7Gl2gt5YQa/JCA" 76 | }, 77 | { 78 | "p@$$w0rd", 79 | <<252, 159, 83, 202, 89, 107, 141, 17, 66, 200, 121, 239, 29, 163, 20, 34>>, 80 | 19_000, 81 | "$pbkdf2-sha512$19000$/J9TyllrjRFCyHnvHaMUIg$AJ3Dr926ltK1sOZMZAAoT7EoR7R/Hp.G6Bt.4DFENiYayhVM/ZBPuqjFNhcE9NjTmceTmLnSqzfEQ8mafy49sw" 82 | }, 83 | { 84 | "oh this is hard 2 guess", 85 | <<1, 96, 140, 17, 162, 84, 42, 165, 84, 42, 165, 244, 62, 71, 136, 177>>, 86 | 19_000, 87 | "$pbkdf2-sha512$19000$AWCMEaJUKqVUKqX0PkeIsQ$F0xkzJUOKaH8pwAfEwLeZK2/li6CF3iEcpfoJ1XoExQUTStXCNVxE1sd1k0aeQlSFK6JnxJOjM18kZIdzNYkcQ" 88 | }, 89 | { 90 | "even more difficult", 91 | <<215, 186, 87, 42, 133, 112, 14, 1, 160, 52, 38, 100, 44, 229, 92, 203>>, 92 | 19_000, 93 | "$pbkdf2-sha512$19000$17pXKoVwDgGgNCZkLOVcyw$TEv9woSaVTsYHLxXnFbWO1oKrUGfUAljkLnqj8W/80BGaFbhccG8B9fZc05RoUo7JQvfcwsNee19g8GD5UxwHA" 94 | } 95 | ] 96 | |> check_vectors 97 | end 98 | 99 | test "Consistency tests for sha512" do 100 | [ 101 | { 102 | "funferal", 103 | <<192, 39, 248, 127, 11, 37, 71, 252, 74, 75, 244, 70, 129, 27, 51, 71>>, 104 | 60_000, 105 | "$pbkdf2-sha512$60000$wCf4fwslR/xKS/RGgRszRw$QJHazw8zTaY0HvGQF1Slb07Ug9DFFLjoq63aORwhA.o/OM.e9UpxldolWyCNLv3duHuxpEWoZtGHfm3VTFCqpg" 106 | }, 107 | { 108 | "he's N0t the Me551ah!", 109 | <<60, 130, 11, 97, 11, 23, 236, 250, 227, 233, 56, 1, 86, 131, 41, 163>>, 110 | 60_000, 111 | "$pbkdf2-sha512$60000$PIILYQsX7Prj6TgBVoMpow$tsPUY4uMzTbJuv81xxZzsUGvT1LGjk9EfJuAYoZH9KaCSGH90J8BuQwY4Jb0JZbwOI00BSR4hDBVmn3Z8V.Ywg" 112 | }, 113 | { 114 | "ἓν οἶδα ὅτι οὐδὲν οἶδα", 115 | <<29, 10, 228, 45, 215, 110, 213, 118, 168, 14, 197, 198, 67, 72, 34, 221>>, 116 | 60_000, 117 | "$pbkdf2-sha512$60000$HQrkLddu1XaoDsXGQ0gi3Q$UVkPApVkIkQN0FTQwaKffYoZ5Mbh0712p1GWs9H1Z.fBNQScUWCj/GAUtZDYMkIN3kIi9ORvut.SQ7aBipcpDQ" 118 | } 119 | ] 120 | |> check_vectors 121 | end 122 | 123 | test "Consistency tests for sha256" do 124 | [ 125 | { 126 | "funferal", 127 | <<192, 39, 248, 127, 11, 37, 71, 252, 74, 75, 244, 70, 129, 27, 51, 71>>, 128 | 60_000, 129 | "$pbkdf2-sha256$60000$wCf4fwslR/xKS/RGgRszRw$p1XmqbB8u/EfvftMDoLyL4ZcVKT6Nz.Y4E/8xuoRePA" 130 | }, 131 | { 132 | "he's N0t the Me551ah!", 133 | <<60, 130, 11, 97, 11, 23, 236, 250, 227, 233, 56, 1, 86, 131, 41, 163>>, 134 | 80_000, 135 | "$pbkdf2-sha256$80000$PIILYQsX7Prj6TgBVoMpow$ErhanHiaHKh63nxft7nMS7rRpglbrZdQ6tEAhyrd.tQ" 136 | }, 137 | { 138 | "ἓν οἶδα ὅτι οὐδὲν οἶδα", 139 | <<29, 10, 228, 45, 215, 110, 213, 118, 168, 14, 197, 198, 67, 72, 34, 221>>, 140 | 100_000, 141 | "$pbkdf2-sha256$100000$HQrkLddu1XaoDsXGQ0gi3Q$egGo.5eQIb9Ulp27Xyc7WkesMu/u4mksXknuExBUCnc" 142 | } 143 | ] 144 | |> check_vectors(:sha256) 145 | end 146 | 147 | test "django format test vectors" do 148 | [ 149 | { 150 | "pa$$word", 151 | "xvJitqXFKLDy", 152 | 20_000, 153 | "pbkdf2_sha256$20000$xvJitqXFKLDy$CEzm5tv/2IVR5vT1pgN1B9ebo3n62xktmhClSuMsrM4=" 154 | }, 155 | { 156 | "passDATAb00AB7YxDTT", 157 | "7T4cGyTsIqXl", 158 | 20_000, 159 | "pbkdf2_sha256$20000$7T4cGyTsIqXl$SGp9lb20DSYXk1SY80NxFlGPOIN8apThVNanlL628aw=" 160 | }, 161 | { 162 | "passDATAb00AB7YxDTTl", 163 | "pOIkJ2DADj78", 164 | 20_000, 165 | "pbkdf2_sha256$20000$pOIkJ2DADj78$6/xhxGrCHGUJsQSs16V5s1GtucMSgGdtfVKmCyJsv58=" 166 | }, 167 | { 168 | "passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE5", 169 | "T1TgNUPEvPnc", 170 | 20_000, 171 | "pbkdf2_sha256$20000$T1TgNUPEvPnc$OBc2b5qo+EoPbkPEr1m4Vcbc8ip2IYG/AfiiIgB4vcQ=" 172 | } 173 | ] 174 | |> check_vectors(:sha256, :django) 175 | end 176 | 177 | test "create a hash in hex format and verify the password" do 178 | password = "password" 179 | salt = "saltSALTsaltSALT" 180 | hash = Base.hash_password(password, salt, format: :hex) 181 | assert Base.verify_pass(password, hash, salt, :sha512, "160000", :hex) 182 | refute Base.verify_pass(password, hash, salt, :sha256, "160000", :hex) 183 | hash = Base.hash_password(password, salt, digest: :sha256, format: :hex) 184 | assert Base.verify_pass(password, hash, salt, :sha256, "160000", :hex) 185 | refute Base.verify_pass(password, hash, salt, :sha256, "100000", :hex) 186 | hash = Base.hash_password(password, salt, digest: :sha256, format: :hex, length: 64) 187 | assert Base.verify_pass(password, hash, salt, :sha256, "160000", :hex) 188 | refute Base.verify_pass(password, hash, salt, :sha512, "160000", :hex) 189 | end 190 | 191 | test "configuring hash_password number of rounds" do 192 | Application.put_env(:pbkdf2_elixir, :rounds, 1) 193 | assert String.starts_with?(Base.hash_password("password", "somesalt"), "$pbkdf2-sha512$1$") 194 | Application.delete_env(:pbkdf2_elixir, :rounds) 195 | 196 | assert String.starts_with?( 197 | Base.hash_password("password", "somesalt"), 198 | "$pbkdf2-sha512$160000$" 199 | ) 200 | end 201 | 202 | test "configuring digest and output format" do 203 | salt = Base.gen_salt() 204 | hash = Base.hash_password("password", salt, digest: :sha256) 205 | assert hash =~ "$pbkdf2-sha256" 206 | salt = Base.gen_salt(format: :django) 207 | hash = Base.hash_password("password", salt, digest: :sha256, format: :django) 208 | assert hash =~ "pbkdf2_sha256" 209 | end 210 | 211 | test "wrong length salt to hash_password" do 212 | assert capture_io(:stderr, fn -> 213 | Base.hash_password("password", "salt") 214 | end) =~ "salt less than 8 bytes long is not recommended" 215 | end 216 | 217 | test "hash_password raises if salt is too long" do 218 | salt = String.duplicate("waytoolooongsalt", 64) <> "a" 219 | 220 | assert_raise ArgumentError, fn -> 221 | Base.hash_password("password", salt) 222 | end 223 | end 224 | 225 | test "raises when password or salt is nil to hash_password" do 226 | assert_raise ArgumentError, fn -> 227 | Base.hash_password(nil, "somesalt") 228 | end 229 | 230 | assert_raise ArgumentError, fn -> 231 | Base.hash_password("password", nil) 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /test/pbkdf2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2Test do 2 | use ExUnit.Case 3 | doctest Pbkdf2 4 | 5 | import Comeonin.BehaviourTestHelper 6 | 7 | test "implementation of Comeonin.PasswordHash behaviour" do 8 | password = Enum.random(ascii_passwords()) 9 | assert correct_password_true(Pbkdf2, password) 10 | assert wrong_password_false(Pbkdf2, password) 11 | end 12 | 13 | test "Comeonin.PasswordHash behaviour with non-ascii characters" do 14 | password = Enum.random(non_ascii_passwords()) 15 | assert correct_password_true(Pbkdf2, password) 16 | assert wrong_password_false(Pbkdf2, password) 17 | end 18 | 19 | test "hash_pwd_salt only contains alphanumeric characters" do 20 | assert String.match?(Pbkdf2.hash_pwd_salt("password"), ~r/^[A-Za-z0-9.$\/\-]*$/) 21 | 22 | assert String.match?( 23 | Pbkdf2.hash_pwd_salt("password", format: :django), 24 | ~r/^[A-Za-z0-9+$_=\/]*$/ 25 | ) 26 | 27 | assert String.match?(Pbkdf2.hash_pwd_salt("password", format: :hex), ~r/^[A-Za-z0-9]*$/) 28 | end 29 | 30 | test "hashes with different lengths are correctly created and verified" do 31 | hash = Pbkdf2.hash_pwd_salt("password", length: 128) 32 | assert Pbkdf2.verify_pass("password", hash) == true 33 | django_hash = Pbkdf2.hash_pwd_salt("password", length: 128, format: :django) 34 | assert Pbkdf2.verify_pass("password", django_hash) == true 35 | end 36 | 37 | test "hashes with different number of rounds are correctly created and verified" do 38 | hash = Pbkdf2.hash_pwd_salt("password", rounds: 100_000) 39 | assert Pbkdf2.verify_pass("password", hash) == true 40 | django_hash = Pbkdf2.hash_pwd_salt("password", rounds: 10000, format: :django) 41 | assert Pbkdf2.verify_pass("password", django_hash) == true 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/reference_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.ReferenceTest do 2 | use ExUnit.Case 3 | 4 | alias Pbkdf2.Base 5 | 6 | def read_file_and_run_tests(filename, digest) do 7 | tests = 8 | Path.expand("support/#{filename}", __DIR__) 9 | |> File.read!() 10 | |> String.split("\n", trim: true) 11 | 12 | for t <- tests do 13 | [password, salt, iterations, dklen, hash] = String.split(t, ",", trim: true) 14 | rounds = String.to_integer(iterations) 15 | length = String.to_integer(dklen) 16 | 17 | assert Base.hash_password( 18 | password, 19 | salt, 20 | rounds: rounds, 21 | digest: digest, 22 | length: length, 23 | format: :hex 24 | ) == hash 25 | end 26 | end 27 | 28 | test "sha256 reference tests" do 29 | read_file_and_run_tests("pbkdf2_sha256_test_vectors", :sha256) 30 | end 31 | 32 | test "sha512 reference tests" do 33 | read_file_and_run_tests("pbkdf2_sha512_test_vectors", :sha512) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/stats_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pbkdf2.StatsTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias Pbkdf2.Stats 7 | 8 | test "print report with default options" do 9 | report = capture_io(fn -> Stats.report() end) 10 | assert report =~ "Digest:\t\tpbkdf2-sha512\n" 11 | assert report =~ "Digest length:\t64\n" 12 | assert report =~ "Hash:\t\t$pbkdf2-sha512$160000$" 13 | assert report =~ "Rounds:\t\t160000\n" 14 | assert report =~ "Verification OK" 15 | end 16 | 17 | test "print report with pbkdf2_sha256" do 18 | report = capture_io(fn -> Stats.report(digest: :sha256) end) 19 | assert report =~ "Digest:\t\tpbkdf2-sha256\n" 20 | assert report =~ "Digest length:\t32\n" 21 | assert report =~ "Hash:\t\t$pbkdf2-sha256$160000$" 22 | assert report =~ "Rounds:\t\t160000\n" 23 | assert report =~ "Verification OK" 24 | end 25 | 26 | test "use custom options" do 27 | report = capture_io(fn -> Stats.report(rounds: 300_000) end) 28 | assert report =~ "Digest:\t\tpbkdf2-sha512\n" 29 | assert report =~ "Digest length:\t64\n" 30 | assert report =~ "Hash:\t\t$pbkdf2-sha512$300000$" 31 | assert report =~ "Rounds:\t\t300000\n" 32 | assert report =~ "Verification OK" 33 | end 34 | 35 | test "print report with django format" do 36 | report = capture_io(fn -> Stats.report(format: :django) end) 37 | assert report =~ "Digest length:\t64\n" 38 | assert report =~ "Hash:\t\tpbkdf2_sha512$160000$" 39 | assert report =~ "Verification OK" 40 | report = capture_io(fn -> Stats.report(digest: :sha256, format: :django) end) 41 | assert report =~ "Digest length:\t32\n" 42 | assert report =~ "Hash:\t\tpbkdf2_sha256$160000$" 43 | assert report =~ "Verification OK" 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/pbkdf2_sha256_test_vectors: -------------------------------------------------------------------------------- 1 | passwordPASSWORDpassword,saltSALTsaltSALTsaltSALTsaltSALTsalt,4096,40,348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9 2 | -------------------------------------------------------------------------------- /test/support/pbkdf2_sha512_test_vectors: -------------------------------------------------------------------------------- 1 | passDATAb00AB7YxDTT,saltKEYbcTcXHCBxtjD,1,64,cbe6088ad4359af42e603c2a33760ef9d4017a7b2aad10af46f992c660a0b461ecb0dc2a79c2570941bea6a08d15d6887e79f32b132e1c134e9525eeddd744fa 2 | passDATAb00AB7YxDTT,saltKEYbcTcXHCBxtjD,100000,64,accdcd8798ae5cd85804739015ef2a11e32591b7b7d16f76819b30b0d49d80e1abea6c9822b80a1fdfe421e26f5603eca8a47a64c9a004fb5af8229f762ff41f 3 | passDATAb00AB7YxDTTl,saltKEYbcTcXHCBxtjD2,1,64,8e5074a9513c1f1512c9b1df1d8bffa9d8b4ef9105dfc16681222839560fb63264bed6aabf761f180e912a66e0b53d65ec88f6a1519e14804eba6dc9df137007 4 | passDATAb00AB7YxDTTl,saltKEYbcTcXHCBxtjD2,100000,64,594256b0bd4d6c9f21a87f7ba5772a791a10e6110694f44365cd94670e57f1aecd797ef1d1001938719044c7f018026697845eb9ad97d97de36ab8786aab5096 5 | passDATAb00AB7YxDTTlR,saltKEYbcTcXHCBxtjD2P,1,64,a6ac8c048a7dfd7b838da88f22c3fab5bff15d7cb8d83a62c6721a8faf6903eab6152cb7421026e36f2ffef661eb4384dc276495c71b5cab72e1c1a38712e56b 6 | passDATAb00AB7YxDTTlR,saltKEYbcTcXHCBxtjD2P,100000,64,94ffc2b1a390b7b8a9e6a44922c330db2b193adcf082eecd06057197f35931a9d0ec0ee5c660744b50b61f23119b847e658d179a914807f4b8ab8eb9505af065 7 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE5,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJe,1,64,e2ccc7827f1dd7c33041a98906a8fd7bae1920a55fcb8f831683f14f1c3979351cb868717e5ab342d9a11acf0b12d3283931d609b06602da33f8377d1f1f9902 8 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE5,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJe,100000,64,07447401c85766e4aed583de2e6bf5a675eabe4f3618281c95616f4fc1fdfe6ecbc1c3982789d4fd941d6584ef534a78bd37ae02555d9455e8f089fdb4dfb6bb 9 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJem,1,64,b029a551117ff36977f283f579dc7065b352266ea243bdd3f920f24d4d141ed8b6e02d96e2d3bdfb76f8d77ba8f4bb548996ad85bb6f11d01a015ce518f9a717 10 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJem,100000,64,31f5cc83ed0e948c05a15735d818703aaa7bff3f09f5169caf5dba6602a05a4d5cff5553d42e82e40516d6dc157b8daeae61d3fea456d964cb2f7f9a63bbbdb5 11 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57U,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemk,1,64,28b8a9f644d6800612197bb74df460272e2276de8cc07ac4897ac24dbc6eb77499fcaf97415244d9a29da83fc347d09a5dbcfd6bd63ff6e410803dca8a900ab6 12 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57U,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemk,100000,64,056bc9072a356b7d4da60dd66f5968c2caa375c0220eda6b47ef8e8d105ed68b44185fe9003fbba49e2c84240c9e8fd3f5b2f4f6512fd936450253db37d10028 13 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi0,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy,1,64,16226c85e4f8d604573008bfe61c10b6947b53990450612dd4a3077f7dee2116229e68efd1df6d73bd3c6d07567790eea1e8b2ae9a1b046be593847d9441a1b7 14 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi0,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy,100000,64,70cf39f14c4caf3c81fa288fb46c1db52d19f72722f7bc84f040676d3371c89c11c50f69bcfbc3acb0ab9e92e4ef622727a916219554b2fa121bedda97ff3332 15 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi04,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy6,1,64,880c58c316d3a5b9f05977ab9c60c10abeebfad5ce89cae62905c1c4f80a0a098d82f95321a6220f8aeccfb45ce6107140899e8d655306ae6396553e2851376c 16 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi04,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy6,100000,64,2668b71b3ca56136b5e87f30e098f6b4371cb5ed95537c7a073dac30a2d5be52756adf5bb2f4320cb11c4e16b24965a9c790def0cbc62906920b4f2eb84d1d4a 17 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi04U,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy6P,1,64,93b9ba8283cc17d50ef3b44820828a258a996de258225d24fb59990a6d0de82dfb3fe2ac201952100e4cc8f06d883a9131419c0f6f5a6ecb8ec821545f14adf1 18 | passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi04U,saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy6P,100000,64,2575b485afdf37c260b8f3386d33a60ed929993c9d48ac516ec66b87e06be54ade7e7c8cb3417c81603b080a8eefc56072811129737ced96236b9364e22ce3a5 19 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------