Packages
macula
0.19.0
7.1.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
Current section
Files
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) ->
%% BLAKE3 streaming via concatenation (pure Erlang)
%% Can be optimized with NIF for incremental hashing
Combined = iolist_to_binary(Chunks),
blake3_hash(Combined).
%% @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
%% Pure Erlang BLAKE3 implementation.
%% This is a simplified version for correctness; production should use NIF.
%%
%% BLAKE3 is based on the Bao tree hashing construction with ChaCha-like
%% compression. For simplicity, we implement the single-threaded version.
-spec blake3_hash(binary()) -> hash().
blake3_hash(Data) ->
%% Try NIF first if available
case try_nif_blake3(Data) of
{ok, Hash} -> Hash;
not_available -> blake3_pure(Data)
end.
%% @private
%% Try to use NIF implementation if macula_nifs is available.
try_nif_blake3(Data) ->
case code:is_loaded(macula_nifs) of
{file, _} ->
case erlang:function_exported(macula_nifs, blake3_hash, 1) of
true -> {ok, macula_nifs:blake3_hash(Data)};
false -> not_available
end;
false ->
not_available
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.