Packages

macula

0.35.0
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_content_system macula_content_hasher.erl
Raw

src/macula_content_system/macula_content_hasher.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% Content hashing module for Macula content-addressed storage.
%%%
%%% Provides cryptographic hashing using BLAKE3 (primary) and SHA256 (fallback).
%%% BLAKE3 uses a pure Erlang implementation that can be optimized with NIFs
%%% from macula-nifs/ for production use.
%%%
%%% == Example Usage ==
%%% ```
%%% %% Hash binary data
%%% Hash = macula_content_hasher:hash(blake3, Data).
%%%
%%% %% Verify hash
%%% true = macula_content_hasher:verify(blake3, Data, Hash).
%%%
%%% %% Stream hash multiple chunks
%%% Hash = macula_content_hasher:hash_streaming(sha256, [Chunk1, Chunk2]).
%%% '''
%%% @end
%%%-------------------------------------------------------------------
-module(macula_content_hasher).
%% API
-export([
hash/2,
hash_streaming/2,
verify/3,
supported_algorithms/0,
is_supported/1,
hash_size/1,
hex_encode/1,
hex_decode/1
]).
%% Types
-type algorithm() :: blake3 | sha256.
-type hash() :: <<_:256>>. %% 32 bytes
-export_type([algorithm/0, hash/0]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Hash binary data using the specified algorithm.
-spec hash(algorithm(), binary()) -> hash().
hash(sha256, Data) ->
crypto:hash(sha256, Data);
hash(blake3, Data) ->
blake3_hash(Data).
%% @doc Hash a list of binary chunks using streaming (memory efficient).
-spec hash_streaming(algorithm(), [binary()]) -> hash().
hash_streaming(sha256, Chunks) ->
Context0 = crypto:hash_init(sha256),
Context1 = lists:foldl(
fun(Chunk, Ctx) -> crypto:hash_update(Ctx, Chunk) end,
Context0,
Chunks
),
crypto:hash_final(Context1);
hash_streaming(blake3, Chunks) ->
%% Use NIF streaming hash if available (avoids concatenation)
case is_nif_available() of
true -> macula_blake3_nif:hash_streaming(Chunks);
false ->
Combined = iolist_to_binary(Chunks),
blake3_pure(Combined)
end.
%% @doc Verify that data matches the expected hash.
-spec verify(algorithm(), binary(), hash()) -> boolean().
verify(Algorithm, Data, ExpectedHash) ->
ActualHash = hash(Algorithm, Data),
ActualHash =:= ExpectedHash.
%% @doc Return list of supported hash algorithms.
-spec supported_algorithms() -> [algorithm()].
supported_algorithms() ->
[blake3, sha256].
%% @doc Check if an algorithm is supported.
-spec is_supported(atom()) -> boolean().
is_supported(blake3) -> true;
is_supported(sha256) -> true;
is_supported(_) -> false.
%% @doc Return hash output size in bytes for an algorithm.
-spec hash_size(algorithm()) -> pos_integer().
hash_size(blake3) -> 32;
hash_size(sha256) -> 32.
%% @doc Encode binary to lowercase hex string.
-spec hex_encode(binary()) -> binary().
hex_encode(Bin) ->
<< <<(hex_digit(N))>> || <<N:4>> <= Bin >>.
%% @doc Decode hex string to binary.
-spec hex_decode(binary()) -> {ok, binary()} | {error, invalid_hex}.
hex_decode(Hex) ->
hex_decode(Hex, <<>>).
%%%===================================================================
%%% Internal Functions - BLAKE3
%%%===================================================================
%% @private
%% BLAKE3 hash with NIF acceleration when available.
%%
%% Tries to use macula_blake3_nif (from macula-nifs package) which provides
%% a Rust NIF implementation that is 10-20x faster than pure Erlang.
%% Falls back to pure Erlang implementation if NIFs are not available.
-spec blake3_hash(binary()) -> hash().
blake3_hash(Data) ->
case is_nif_available() of
true -> macula_blake3_nif:hash(Data);
false -> blake3_pure(Data)
end.
%% @private
%% Check if BLAKE3 NIF is available.
%% Caches result in process dictionary for performance.
is_nif_available() ->
case get(macula_blake3_nif_available) of
undefined ->
Available = check_nif_available(),
put(macula_blake3_nif_available, Available),
Available;
Cached ->
Cached
end.
%% @private
check_nif_available() ->
case code:ensure_loaded(macula_blake3_nif) of
{module, macula_blake3_nif} ->
case erlang:function_exported(macula_blake3_nif, is_nif_loaded, 0) of
true -> macula_blake3_nif:is_nif_loaded();
false -> false
end;
_ ->
false
end.
%% @private
%% Pure Erlang BLAKE3 implementation.
%% Based on the BLAKE3 specification with key constants from ChaCha.
blake3_pure(Data) ->
%% BLAKE3 IV (same as BLAKE2s IV)
IV = {
16#6A09E667, 16#BB67AE85,
16#3C6EF372, 16#A54FF53A,
16#510E527F, 16#9B05688C,
16#1F83D9AB, 16#5BE0CD19
},
%% BLAKE3 constants
BlockLen = 64,
ChunkLen = 1024,
%% Flag constants
RootFlag = 8,
%% Process data in chunks
Chunks = chunk_data(Data, ChunkLen),
ChunkCount = length(Chunks),
%% Hash each chunk
ChunkHashes = lists:map(
fun({Index, Chunk}) ->
IsFirst = Index =:= 0,
IsLast = Index =:= ChunkCount - 1,
Flags = chunk_flags(IsFirst, IsLast, ChunkCount =:= 1),
compress_chunk(IV, Chunk, BlockLen, Flags)
end,
lists:zip(lists:seq(0, ChunkCount - 1), Chunks)
),
%% Build Merkle tree from chunk hashes
finalize_tree(ChunkHashes, IV, RootFlag).
%% @private
chunk_data(<<>>, _ChunkLen) ->
[<<>>];
chunk_data(Data, ChunkLen) ->
chunk_data(Data, ChunkLen, []).
chunk_data(<<>>, _ChunkLen, Acc) ->
lists:reverse(Acc);
chunk_data(Data, ChunkLen, Acc) when byte_size(Data) =< ChunkLen ->
lists:reverse([Data | Acc]);
chunk_data(Data, ChunkLen, Acc) ->
<<Chunk:ChunkLen/binary, Rest/binary>> = Data,
chunk_data(Rest, ChunkLen, [Chunk | Acc]).
%% @private
chunk_flags(true, true, true) -> 1 bor 2 bor 8; %% START | END | ROOT
chunk_flags(true, true, false) -> 1 bor 2; %% START | END
chunk_flags(true, false, _) -> 1; %% START
chunk_flags(false, true, _) -> 2; %% END
chunk_flags(false, false, _) -> 0.
%% @private
%% Compress a single chunk using BLAKE3 compression.
compress_chunk(IV, Chunk, BlockLen, Flags) ->
%% Pad chunk to block boundary
PaddedLen = ((byte_size(Chunk) + BlockLen - 1) div BlockLen) * BlockLen,
Padded = case byte_size(Chunk) < PaddedLen of
true -> <<Chunk/binary, 0:((PaddedLen - byte_size(Chunk)) * 8)>>;
false -> Chunk
end,
%% Process blocks
Blocks = chunk_data(Padded, BlockLen),
BlockCount = length(Blocks),
State = lists:foldl(
fun({BlockIdx, Block}, S) ->
IsLastBlock = BlockIdx =:= BlockCount - 1,
BlockFlags = if IsLastBlock -> Flags; true -> 0 end,
compress_block(S, Block, byte_size(Chunk), BlockFlags)
end,
IV,
lists:zip(lists:seq(0, BlockCount - 1), Blocks)
),
%% Extract first 32 bytes of state as hash
state_to_hash(State).
%% @private
%% BLAKE3 block compression function (simplified G function).
compress_block({H0, H1, H2, H3, H4, H5, H6, H7}, Block, Counter, Flags) ->
%% Message schedule from block
M = block_to_words(Block),
%% Initial state: h[0..7] || IV[0..3] || counter_lo || counter_hi || block_len || flags
V = {
H0, H1, H2, H3, H4, H5, H6, H7,
16#6A09E667, 16#BB67AE85, 16#3C6EF372, 16#A54FF53A,
Counter band 16#FFFFFFFF,
(Counter bsr 32) band 16#FFFFFFFF,
byte_size(Block),
Flags
},
%% 7 rounds of mixing
V1 = rounds(V, M, 7),
%% XOR upper and lower halves
{V10, V11, V12, V13, V14, V15, V16, V17,
V18, V19, V1A, V1B, V1C, V1D, V1E, V1F} = V1,
{
V10 bxor V18, V11 bxor V19, V12 bxor V1A, V13 bxor V1B,
V14 bxor V1C, V15 bxor V1D, V16 bxor V1E, V17 bxor V1F
}.
%% @private
block_to_words(Block) when byte_size(Block) < 64 ->
Padded = <<Block/binary, 0:((64 - byte_size(Block)) * 8)>>,
block_to_words(Padded);
block_to_words(<<W0:32/little, W1:32/little, W2:32/little, W3:32/little,
W4:32/little, W5:32/little, W6:32/little, W7:32/little,
W8:32/little, W9:32/little, WA:32/little, WB:32/little,
WC:32/little, WD:32/little, WE:32/little, WF:32/little>>) ->
{W0, W1, W2, W3, W4, W5, W6, W7, W8, W9, WA, WB, WC, WD, WE, WF}.
%% @private
rounds(V, _M, 0) -> V;
rounds(V, M, N) ->
V1 = round_fn(V, M),
rounds(V1, permute(M), N - 1).
%% @private
%% BLAKE3 round function with G mixing.
round_fn({V0, V1, V2, V3, V4, V5, V6, V7,
V8, V9, VA, VB, VC, VD, VE, VF},
{M0, M1, M2, M3, M4, M5, M6, M7, _, _, _, _, _, _, _, _}) ->
%% Column mixing
{V0a, V4a, V8a, VCa} = g(V0, V4, V8, VC, M0, M1),
{V1a, V5a, V9a, VDa} = g(V1, V5, V9, VD, M2, M3),
{V2a, V6a, VAa, VEa} = g(V2, V6, VA, VE, M4, M5),
{V3a, V7a, VBa, VFa} = g(V3, V7, VB, VF, M6, M7),
%% Diagonal mixing
{V0b, V5b, VAb, VFb} = g(V0a, V5a, VAa, VFa, M0, M1),
{V1b, V6b, VBb, VCb} = g(V1a, V6a, VBa, VCa, M2, M3),
{V2b, V7b, V8b, VDb} = g(V2a, V7a, V8a, VDa, M4, M5),
{V3b, V4b, V9b, VEb} = g(V3a, V4a, V9a, VEa, M6, M7),
{V0b, V1b, V2b, V3b, V4b, V5b, V6b, V7b,
V8b, V9b, VAb, VBb, VCb, VDb, VEb, VFb}.
%% @private
%% G mixing function.
g(A, B, C, D, MX, MY) ->
A1 = (A + B + MX) band 16#FFFFFFFF,
D1 = rotr32(D bxor A1, 16),
C1 = (C + D1) band 16#FFFFFFFF,
B1 = rotr32(B bxor C1, 12),
A2 = (A1 + B1 + MY) band 16#FFFFFFFF,
D2 = rotr32(D1 bxor A2, 8),
C2 = (C1 + D2) band 16#FFFFFFFF,
B2 = rotr32(B1 bxor C2, 7),
{A2, B2, C2, D2}.
%% @private
rotr32(X, N) ->
((X bsr N) bor (X bsl (32 - N))) band 16#FFFFFFFF.
%% @private
%% Message word permutation for BLAKE3.
permute({M0, M1, M2, M3, M4, M5, M6, M7,
M8, M9, MA, MB, MC, MD, ME, MF}) ->
{M2, M6, M3, MA, M7, M0, M4, MD,
M1, MB, MC, M5, M9, ME, MF, M8}.
%% @private
state_to_hash({H0, H1, H2, H3, H4, H5, H6, H7}) ->
<<H0:32/little, H1:32/little, H2:32/little, H3:32/little,
H4:32/little, H5:32/little, H6:32/little, H7:32/little>>.
%% @private
finalize_tree([Hash], _IV, _RootFlag) ->
Hash;
finalize_tree(Hashes, IV, RootFlag) ->
%% Pair up hashes and compress
Paired = pair_hashes(Hashes),
NewHashes = lists:map(
fun({Left, Right}) ->
Combined = <<Left/binary, Right/binary>>,
compress_chunk(IV, Combined, 64, RootFlag)
end,
Paired
),
finalize_tree(NewHashes, IV, RootFlag).
%% @private
pair_hashes([]) -> [];
pair_hashes([H]) -> [{H, <<0:256>>}]; %% Pad with zero hash
pair_hashes([H1, H2 | Rest]) -> [{H1, H2} | pair_hashes(Rest)].
%%%===================================================================
%%% Internal Functions - Hex Encoding
%%%===================================================================
%% @private
hex_digit(N) when N < 10 -> $0 + N;
hex_digit(N) -> $a + N - 10.
%% @private
hex_decode(<<>>, Acc) ->
{ok, Acc};
hex_decode(<<H1, H2, Rest/binary>>, Acc) ->
case {hex_value(H1), hex_value(H2)} of
{{ok, V1}, {ok, V2}} ->
Byte = (V1 bsl 4) bor V2,
hex_decode(Rest, <<Acc/binary, Byte>>);
_ ->
{error, invalid_hex}
end;
hex_decode(_, _) ->
{error, invalid_hex}.
%% @private
hex_value(C) when C >= $0, C =< $9 -> {ok, C - $0};
hex_value(C) when C >= $a, C =< $f -> {ok, C - $a + 10};
hex_value(C) when C >= $A, C =< $F -> {ok, C - $A + 10};
hex_value(_) -> error.