%% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*-
%% vim: ts=4 sw=4 ft=erlang noet
%%%-------------------------------------------------------------------
%%% @author Andrew Bennett <potatosaladx@gmail.com>
%%% @copyright 2014-2022, Andrew Bennett
%%% @doc Concat KDF, as defined in Section 5.8.1 of NIST.800-56A
%%% See NIST.800-56A: https://dx.doi.org/10.6028/NIST.SP.800-56Ar2
%%% @end
%%% Created :  24 Jul 2015 by Andrew Bennett <potatosaladx@gmail.com>
%%%-------------------------------------------------------------------
-module(jose_jwa_concat_kdf).

%% API
-export([kdf/3]).
-export([kdf/4]).

%%====================================================================
%% API functions
%%====================================================================

kdf(Hash, Z, OtherInfo) ->
	HashFun = resolve_hash(Hash),
	KeyDataLen = bit_size(HashFun(<<>>)),
	kdf(HashFun, Z, OtherInfo, KeyDataLen).

kdf(Hash, Z, OtherInfo, KeyDataLen)
		when is_function(Hash)
		andalso is_binary(Z)
		andalso is_binary(OtherInfo)
		andalso is_integer(KeyDataLen) ->
	HashLen = bit_size(Hash(<<>>)),
	Reps = ceiling(KeyDataLen / HashLen),
	case Reps of
		1 ->
			Concatenation = << 0, 0, 0, 1, Z/binary, OtherInfo/binary >>,
			<< DerivedKey:KeyDataLen/bitstring, _/bitstring >> = Hash(Concatenation),
			DerivedKey;
		_ when Reps > 16#FFFFFFFF ->
			erlang:error({badarg, [Hash, Z, OtherInfo, KeyDataLen]});
		_ ->
			derive_key(Hash, 1, Reps, KeyDataLen, << Z/binary, OtherInfo/binary >>, <<>>)
	end;
kdf(Hash, Z, OtherInfo, KeyDataLen)
		when is_tuple(Hash)
		orelse is_atom(Hash) ->
	kdf(resolve_hash(Hash), Z, OtherInfo, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, <<>>}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, <<>>}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, SuppPrivInfo}, KeyDataLen)
		when is_binary(AlgorithmID)
		andalso is_binary(PartyUInfo)
		andalso is_binary(PartyVInfo)
		andalso is_binary(SuppPubInfo)
		andalso is_binary(SuppPrivInfo) ->
	kdf(Hash, Z, <<
		(byte_size(AlgorithmID)):1/unsigned-big-integer-unit:32, AlgorithmID/binary,
		(byte_size(PartyUInfo)):1/unsigned-big-integer-unit:32, PartyUInfo/binary,
		(byte_size(PartyVInfo)):1/unsigned-big-integer-unit:32, PartyVInfo/binary,
		SuppPubInfo/binary,
		SuppPrivInfo/binary
	>>, KeyDataLen);
kdf(Hash, Z, {undefined, PartyUInfo, PartyVInfo, SuppPubInfo, SuppPrivInfo}, KeyDataLen) ->
	kdf(Hash, Z, {<<>>, PartyUInfo, PartyVInfo, SuppPubInfo, SuppPrivInfo}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, undefined, PartyVInfo, SuppPubInfo, SuppPrivInfo}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, <<>>, PartyVInfo, SuppPubInfo, SuppPrivInfo}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, undefined, SuppPubInfo, SuppPrivInfo}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, PartyUInfo, <<>>, SuppPubInfo, SuppPrivInfo}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, undefined, SuppPrivInfo}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, <<>>, SuppPrivInfo}, KeyDataLen);
kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, undefined}, KeyDataLen) ->
	kdf(Hash, Z, {AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, <<>>}, KeyDataLen).

%%%-------------------------------------------------------------------
%%% Internal functions
%%%-------------------------------------------------------------------

%% @private
ceiling(X) when X < 0 ->
	trunc(X);
ceiling(X) ->
	T = trunc(X),
	case X - T == 0 of
		false ->
			T + 1;
		true ->
			T
	end.

%% @private
derive_key(Hash, Reps, Reps, KeyDataLen, ZOtherInfo, DerivedKeyingMaterial) ->
	Concatenation = << Reps:1/unsigned-big-integer-unit:32, ZOtherInfo/binary >>,
	<< DerivedKey:KeyDataLen/bitstring, _/bitstring >> = << DerivedKeyingMaterial/binary, (Hash(Concatenation))/binary >>,
	DerivedKey;
derive_key(Hash, Counter, Reps, KeyDataLen, ZOtherInfo, DerivedKeyingMaterial) ->
	Concatenation = << Counter:1/unsigned-big-integer-unit:32, ZOtherInfo/binary >>,
	derive_key(Hash, Counter + 1, Reps, KeyDataLen, ZOtherInfo, << DerivedKeyingMaterial/binary, (Hash(Concatenation))/binary >>).

%% @private
resolve_hash(HashFun) when is_function(HashFun) ->
	HashFun;
resolve_hash(DigestType) when is_atom(DigestType) ->
	fun(Data) ->
		crypto:hash(DigestType, Data)
	end;
resolve_hash({hmac, DigestType, Key}) when is_atom(DigestType) ->
	fun(Data) ->
		jose_crypto_compat:mac(hmac, DigestType, Key, Data)
	end.
