| %% @author Hunter Morris <huntermorris@gmail.com> |
| %% @copyright 2008 Hunter Morris |
| %% |
| %% @doc Wrapper around the OpenBSD Blowfish password hashing algorithm, as |
| %% described in "A Future-Adaptable Password Scheme" by Niels Provos and |
| %% David Mazieres: http://www.openbsd.org/papers/bcrypt-paper.ps |
| %% @end |
| %% |
| %% Permission to use, copy, modify, and distribute this software for any |
| %% purpose with or without fee is hereby granted, provided that the above |
| %% copyright notice and this permission notice appear in all copies. |
| |
| %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
| -module(bcrypt). |
| -author('Hunter Morris <huntermorris@gmail.com>'). |
| |
| -behaviour(gen_server). |
| |
| -export([init/1, handle_request/2]). |
| |
| %% API |
| -export([start_link/1, stop/1]). |
| -export([gen_salt/1, gen_salt/2]). |
| -export([hashpw/3]). |
| |
| %% gen_server callbacks |
| -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, |
| code_change/3]). |
| |
| -define(CMD_SALT, 0). |
| -define(CMD_HASHPW, 1). |
| -record(state, {port}). |
| |
| -define(BCRYPT_ERROR(F, D), error_logger:error_msg(F, D)). |
| -define(BCRYPT_WARNING(F, D), error_logger:warning_msg(F, D)). |
| -define(DEFAULT_LOG_ROUNDS, 12). |
| -define(MAX_LOG_ROUNDS(L), L < 32). |
| -define(MIN_LOG_ROUNDS(L), L > 3). |
| |
| %%==================================================================== |
| %% API |
| %%==================================================================== |
| start_link(Filename) -> |
| gen_server:start_link(?MODULE, [{filename, Filename}], []). |
| |
| stop(Pid) when is_pid(Pid) -> |
| gen_server:call(Pid, stop). |
| |
| gen_salt(Pid) when is_pid(Pid) -> |
| gen_salt(Pid, ?DEFAULT_LOG_ROUNDS). |
| |
| gen_salt(Pid, LogRounds) when is_pid(Pid), is_integer(LogRounds), |
| ?MAX_LOG_ROUNDS(LogRounds), |
| ?MIN_LOG_ROUNDS(LogRounds) -> |
| R = crypto:rand_bytes(16), |
| gen_server:call(Pid, {encode_salt, R, LogRounds}). |
| |
| hashpw(Pid, Password, Salt) when is_pid(Pid) -> |
| gen_server:call(Pid, {hashpw, Password, Salt}). |
| |
| %%==================================================================== |
| %% gen_server callbacks |
| %%==================================================================== |
| init(L) -> |
| Filename = proplists:get_value(filename, L), |
| case file:read_file_info(Filename) of |
| {ok, _Info} -> |
| Port = open_port({spawn, Filename}, [{packet, 2}, binary, exit_status]), |
| {ok, #state{port=Port}}; |
| {error, Reason} -> |
| ?BCRYPT_ERROR("Can't open file ~p: ~p", [Filename, Reason]), |
| error |
| end. |
| |
| terminate(_Reason, #state{port=Port}) -> |
| catch port_close(Port), |
| ok. |
| |
| handle_call({encode_salt, R, LogRounds}, From, State) -> |
| Port = State#state.port, |
| Data = term_to_binary({?CMD_SALT, From, {R, LogRounds}}), |
| port_command(Port, Data), |
| {noreply, State}; |
| handle_call({hashpw, Password, Salt}, From, State) -> |
| Port = State#state.port, |
| Data = term_to_binary({?CMD_HASHPW, From, {Password, Salt}}), |
| port_command(Port, Data), |
| {noreply, State}; |
| handle_call(stop, _From, State) -> |
| {stop, normal, ok, State}; |
| handle_call(_Request, _From, State) -> |
| {reply, bad_request, State}. |
| |
| handle_info({Port, {data, Data}}, #state{port=Port}=State) -> |
| case binary_to_term(Data) of |
| {Cmd, To, Reply} when Cmd == ?CMD_SALT; Cmd == ?CMD_HASHPW -> |
| gen_server:reply(To, Reply); |
| Err -> |
| ?BCRYPT_ERROR("Got invalid reply from ~p: ~p", [Port, Err]) |
| end, |
| {noreply, State}; |
| handle_info({Port, {exit_status, Status}}, #state{port=Port}=State) -> |
| %% Rely on whomever is supervising this process to restart. |
| ?BCRYPT_WARNING("Port died: ~p", [Status]), |
| {stop, port_died, State}; |
| handle_info(Msg, State) -> |
| ?BCRYPT_WARNING("Got unexpected message: ~p", [Msg]), |
| {noreply, State}. |
| |
| handle_cast(_Msg, State) -> |
| {noreply, State}. |
| |
| code_change(_OldVsn, State, _Extra) -> |
| {ok, State}. |