src/bcrypt_port.erl

%% @copyright 2011 Hunter Morris
%% @doc Implementation of `gen_server' behaviour.
%% @end
%% Distributed under the MIT license; see LICENSE for details.
-module(bcrypt_port).
-author('Hunter Morris <hunter.morris@smarkets.com>').

-behaviour(gen_server).

%% API
-export([start_link/0, stop/0]).
-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]).

-record(state, {
    port :: port(),
    default_log_rounds :: non_neg_integer(),
    cmd_from :: {pid(), term()} | undefined
    }).

-type state() :: #state{port :: port(), 
	default_log_rounds :: non_neg_integer(), 
	cmd_from :: {pid(), term()} | undefined}.

-define(CMD_SALT, 0).
-define(CMD_HASH, 1).
-define(BCRYPT_ERROR(F, D), error_logger:error_msg(F, D)).
-define(BCRYPT_WARNING(F, D), error_logger:warning_msg(F, D)).

-spec start_link() -> Result when
	Result :: {ok,Pid} | ignore | {error,Error},
	Pid :: pid(),
	Error :: {already_started,Pid} | term(),
	Pid :: pid().
start_link() ->
    Dir = case code:priv_dir(bcrypt) of
              {error, bad_name} ->
                  case code:which(bcrypt) of
                      Filename when is_list(Filename) ->
                          filename:join(
                            [filename:dirname(Filename), "../priv"]);
                      _ ->
                          "../priv"
                  end;
              Priv -> Priv
          end,
    Port = filename:join(Dir, "bcrypt"),
    gen_server:start_link(?MODULE, [Port], []).

-spec stop() -> Result when
	Result :: {stop, normal, ok, state()}.
stop() -> gen_server:call(?MODULE, stop).

-spec gen_salt(Pid) -> Result when
	Pid :: pid(),
	Result :: {ok, Salt},
	Salt :: [byte()].
gen_salt(Pid) ->
    R = crypto:strong_rand_bytes(16),
    gen_server:call(Pid, {encode_salt, R}, infinity).

-spec gen_salt(Pid, LogRounds) -> Result when
	Pid :: pid(),
	LogRounds :: bcrypt:rounds(),
	Result :: {ok, Salt},
	Salt :: [byte()].
gen_salt(Pid, LogRounds) ->
    R = crypto:strong_rand_bytes(16),
    gen_server:call(Pid, {encode_salt, R, LogRounds}, infinity).

-spec hashpw(Pid, Password, Salt) -> Result when
	Pid :: pid(),
	Password :: [byte()],
	Salt :: [byte()],
	Result :: [byte()].
hashpw(Pid, Password, Salt) ->
    gen_server:call(Pid, {hashpw, Password, Salt}, infinity).

%%====================================================================
%% gen_server callbacks
%%====================================================================
%% @private

init([Filename]) ->
    case file:read_file_info(Filename) of
        {ok, _Info} ->
            Port = open_port(
                     {spawn, Filename}, [{packet, 2}, binary, exit_status]),
            ok = bcrypt_pool:available(self()),
            {ok, Rounds} = application:get_env(bcrypt, default_log_rounds),
            {ok, #state{port = Port, default_log_rounds = Rounds}};
        {error, Reason} ->
            ?BCRYPT_ERROR("Can't open file ~p: ~p", [Filename, Reason]),
            {stop, error_opening_bcrypt_file}
    end.

%% @private

terminate(_Reason, #state{port=Port}) ->
    catch port_close(Port),
    ok.

%% @private

handle_call({encode_salt, R}, From, #state{default_log_rounds = LogRounds} = State) ->
    handle_call({encode_salt, R, LogRounds}, From, State);
handle_call({encode_salt, R, LogRounds}, From, #state{ cmd_from = undefined } = State) ->
    Port = State#state.port,
    Data = term_to_binary({?CMD_SALT, {iolist_to_binary(R), LogRounds}}),
    erlang:port_command(Port, Data),
    {noreply, State#state{ cmd_from = From }};
handle_call({encode_salt, _R, _Rounds}, From, #state{ cmd_from = CmdFrom } = State) ->
    ?BCRYPT_ERROR("bcrypt: Salt request from ~p whilst busy for ~p", [ From, CmdFrom ]),
    {reply, {error, {busy, From}}, State};
handle_call({hashpw, Password, Salt}, From, #state{ cmd_from = undefined } = State) ->
    Port = State#state.port,
    Data = term_to_binary({?CMD_HASH, {iolist_to_binary(Password), iolist_to_binary(Salt)}}),
    erlang:port_command(Port, Data),
    {noreply, State#state{ cmd_from = From }};
handle_call({hashpw, _Password, _Salt}, From, #state{ cmd_from = CmdFrom } = State) ->
    ?BCRYPT_ERROR("bcrypt: Hash request from ~p whilst busy for ~p", [ From, CmdFrom ]),
    {reply, {error, {busy, From}}, State};
handle_call(stop, _From, State) ->
    {stop, normal, ok, State};
handle_call(Msg, _, State) ->
    {stop, {unknown_call, Msg}, State}.

handle_cast(Msg, State) ->
    {stop, {unknown_cast, Msg}, State}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

handle_info({Port, {data, Data}}, #state{ port = Port, cmd_from = From } = State) ->
    Reply =
        case binary_to_term(Data) of
            {_, Error} when is_atom(Error) ->
                {error, Error};
            {?CMD_SALT, Result} when is_binary(Result) ->
                {ok, binary_to_list(Result)};
            {?CMD_HASH, Result} when is_binary(Result) ->
                {ok, binary_to_list(Result)}
        end,
    gen_server:reply(From, Reply),
    ok = bcrypt_pool:available(self()),
    {noreply, State#state{ cmd_from = undefined }};
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, _) ->
    exit({unknown_info, Msg}).