Packages
macula
1.4.11
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_crypto_nif.erl
%% @doc Cryptographic operations for Macula mesh.
%%
%% This module provides Ed25519 key generation, signing, and verification,
%% BLAKE3 and SHA-256 hashing, and base64 encoding. It uses Rust NIFs when
%% available, falling back to pure Erlang implementations otherwise.
%%
%% == NIF vs Erlang ==
%%
%% The Rust NIFs provide significant performance improvements:
%% - Key generation: ~10x faster
%% - Signing: ~5x faster
%% - Verification: ~8x faster
%% - BLAKE3: ~20x faster
%% - SHA-256: ~3x faster
%%
%% The pure Erlang fallbacks ensure the module works even when NIFs
%% cannot be loaded (e.g., different architecture, missing Rust toolchain).
%%
%% @author rgfaber
-module(macula_crypto_nif).
%% API
-export([
generate_keypair/0,
sign/2,
verify/3,
blake3/1,
blake3_streaming/1,
blake3_verify/2,
blake3_hex/1,
sha256/1,
sha256_base64/1,
base64_encode/1,
base64_decode/1,
secure_compare/2,
is_nif_loaded/0
]).
%% NIF stubs
-export([
nif_generate_keypair/0,
nif_sign/2,
nif_verify/3,
nif_blake3/1,
nif_blake3_streaming/1,
nif_blake3_verify/2,
nif_blake3_hex/1,
nif_sha256/1,
nif_sha256_base64/1,
nif_base64_encode/1,
nif_base64_decode/1,
nif_secure_compare/2
]).
-on_load(init/0).
-define(NIF_LOADED_KEY, macula_crypto_nif_loaded).
%%====================================================================
%% Init
%%====================================================================
init() ->
PrivDir = case code:priv_dir(macula) of
{error, _} ->
case code:which(?MODULE) of
Filename when is_list(Filename) ->
filename:join(filename:dirname(filename:dirname(Filename)), "priv");
_ ->
"priv"
end;
Dir ->
Dir
end,
Path = filename:join(PrivDir, "macula_crypto_nif"),
case erlang:load_nif(Path, 0) of
ok ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, {reload, _}} ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, _Reason} ->
%% NIF not available, will use Erlang fallbacks
ok
end.
%%====================================================================
%% API
%%====================================================================
%% @doc Check if the NIF is loaded.
-spec is_nif_loaded() -> boolean().
is_nif_loaded() ->
persistent_term:get(?NIF_LOADED_KEY, false).
%% @doc Generate a new Ed25519 keypair.
%% Returns `{ok, {PublicKey, PrivateKey}}' where both are 32-byte binaries.
-spec generate_keypair() -> {ok, {PubKey :: binary(), PrivKey :: binary()}}.
generate_keypair() ->
case is_nif_loaded() of
true -> nif_generate_keypair();
false -> erlang_generate_keypair()
end.
%% @doc Sign a message with an Ed25519 private key.
%% Returns `{ok, Signature}' where Signature is a 64-byte binary,
%% or `{error, invalid_private_key}'.
-spec sign(Message :: binary(), PrivateKey :: binary()) ->
{ok, Signature :: binary()} | {error, invalid_private_key}.
sign(Message, PrivateKey) ->
case is_nif_loaded() of
true ->
case nif_sign(Message, PrivateKey) of
{ok, Sig} -> {ok, Sig};
{invalid_private_key, _} -> {error, invalid_private_key}
end;
false -> erlang_sign(Message, PrivateKey)
end.
%% @doc Verify an Ed25519 signature.
%% Returns `true' if valid, `false' otherwise.
-spec verify(Message :: binary(), Signature :: binary(), PublicKey :: binary()) -> boolean().
verify(Message, Signature, PublicKey) ->
case is_nif_loaded() of
true -> nif_verify(Message, Signature, PublicKey);
false -> erlang_verify(Message, Signature, PublicKey)
end.
%% @doc Compute BLAKE3 hash.
%% Returns 32-byte hash binary.
-spec blake3(Data :: binary()) -> Hash :: binary().
blake3(Data) ->
case is_nif_loaded() of
true -> nif_blake3(Data);
false -> erlang_blake3(Data)
end.
%% @doc Compute BLAKE3 hash of multiple chunks (streaming).
%% Returns 32-byte hash binary.
-spec blake3_streaming(Chunks :: [binary()]) -> Hash :: binary().
blake3_streaming(Chunks) ->
case is_nif_loaded() of
true -> nif_blake3_streaming(Chunks);
false -> erlang_blake3(iolist_to_binary(Chunks))
end.
%% @doc Verify data matches expected BLAKE3 hash.
-spec blake3_verify(Data :: binary(), ExpectedHash :: binary()) -> boolean().
blake3_verify(Data, ExpectedHash) ->
case is_nif_loaded() of
true -> nif_blake3_verify(Data, ExpectedHash);
false -> erlang_blake3(Data) =:= ExpectedHash
end.
%% @doc Compute BLAKE3 hash and return as hex string.
%% Returns 64-character hex string.
-spec blake3_hex(Data :: binary()) -> HexHash :: binary().
blake3_hex(Data) ->
case is_nif_loaded() of
true -> nif_blake3_hex(Data);
false -> hex_encode(erlang_blake3(Data))
end.
%% @doc Compute SHA-256 hash.
%% Returns 32-byte hash binary.
-spec sha256(Data :: binary()) -> Hash :: binary().
sha256(Data) ->
case is_nif_loaded() of
true -> nif_sha256(Data);
false -> erlang_sha256(Data)
end.
%% @doc Compute SHA-256 hash and encode as URL-safe base64.
%% Returns base64-encoded string (no padding).
-spec sha256_base64(Data :: binary()) -> Base64Hash :: binary().
sha256_base64(Data) ->
case is_nif_loaded() of
true -> nif_sha256_base64(Data);
false -> erlang_sha256_base64(Data)
end.
%% @doc Encode data as URL-safe base64 (no padding).
-spec base64_encode(Data :: binary()) -> Encoded :: binary().
base64_encode(Data) ->
case is_nif_loaded() of
true -> nif_base64_encode(Data);
false -> erlang_base64_encode(Data)
end.
%% @doc Decode URL-safe base64 data.
%% Returns `{ok, Data}' or `{error, invalid_base64}'.
-spec base64_decode(Encoded :: binary()) -> {ok, binary()} | {error, atom()}.
base64_decode(Encoded) ->
case is_nif_loaded() of
true ->
case nif_base64_decode(Encoded) of
{ok, Data} -> {ok, Data};
{error, _} -> {error, invalid_base64}
end;
false -> erlang_base64_decode(Encoded)
end.
%% @doc Constant-time comparison of two binaries.
%% Important for security - prevents timing attacks.
-spec secure_compare(A :: binary(), B :: binary()) -> boolean().
secure_compare(A, B) ->
case is_nif_loaded() of
true -> nif_secure_compare(A, B);
false -> erlang_secure_compare(A, B)
end.
%%====================================================================
%% NIF Stubs (replaced when NIF loads)
%%====================================================================
nif_generate_keypair() ->
erlang:nif_error(nif_not_loaded).
nif_sign(_Message, _PrivateKey) ->
erlang:nif_error(nif_not_loaded).
nif_verify(_Message, _Signature, _PublicKey) ->
erlang:nif_error(nif_not_loaded).
nif_blake3(_Data) ->
erlang:nif_error(nif_not_loaded).
nif_blake3_streaming(_Chunks) ->
erlang:nif_error(nif_not_loaded).
nif_blake3_verify(_Data, _ExpectedHash) ->
erlang:nif_error(nif_not_loaded).
nif_blake3_hex(_Data) ->
erlang:nif_error(nif_not_loaded).
nif_sha256(_Data) ->
erlang:nif_error(nif_not_loaded).
nif_sha256_base64(_Data) ->
erlang:nif_error(nif_not_loaded).
nif_base64_encode(_Data) ->
erlang:nif_error(nif_not_loaded).
nif_base64_decode(_Encoded) ->
erlang:nif_error(nif_not_loaded).
nif_secure_compare(_A, _B) ->
erlang:nif_error(nif_not_loaded).
%%====================================================================
%% Pure Erlang Fallbacks
%%====================================================================
%% @private Generate keypair using Erlang crypto
erlang_generate_keypair() ->
{PubKey, PrivKey} = crypto:generate_key(eddsa, ed25519),
%% PrivKey from crypto is 64 bytes (seed + public), we want just the 32-byte seed
<<Seed:32/binary, _/binary>> = PrivKey,
{ok, {PubKey, Seed}}.
%% @private Sign using Erlang crypto
erlang_sign(Message, PrivateKey) when byte_size(PrivateKey) =:= 32 ->
%% Erlang crypto expects 64-byte private key for eddsa
%% We reconstruct it from seed
try
%% Generate the full key from seed to get the public key portion
{PubKey, _} = crypto:generate_key(eddsa, ed25519, PrivateKey),
FullPrivKey = <<PrivateKey/binary, PubKey/binary>>,
Signature = crypto:sign(eddsa, none, Message, [FullPrivKey, ed25519]),
{ok, Signature}
catch
_:_ -> {error, invalid_private_key}
end;
erlang_sign(_Message, _PrivateKey) ->
{error, invalid_private_key}.
%% @private Verify using Erlang crypto
erlang_verify(Message, Signature, PublicKey) when byte_size(Signature) =:= 64,
byte_size(PublicKey) =:= 32 ->
try
crypto:verify(eddsa, none, Message, Signature, [PublicKey, ed25519])
catch
_:_ -> false
end;
erlang_verify(_Message, _Signature, _PublicKey) ->
false.
%% @private SHA-256 using Erlang crypto
erlang_sha256(Data) ->
crypto:hash(sha256, Data).
%% @private SHA-256 + base64 encode
erlang_sha256_base64(Data) ->
Hash = crypto:hash(sha256, Data),
erlang_base64_encode(Hash).
%% @private URL-safe base64 encode (no padding)
erlang_base64_encode(Data) ->
%% Standard base64 encode
B64 = base64:encode(Data),
%% Make URL-safe: + -> -, / -> _
B64_Url = binary:replace(binary:replace(B64, <<"+">>, <<"-">>, [global]), <<"/">>, <<"_">>, [global]),
%% Remove padding
binary:replace(B64_Url, <<"=">>, <<>>, [global]).
%% @private URL-safe base64 decode
erlang_base64_decode(Encoded) ->
try
%% Restore standard base64: - -> +, _ -> /
B64_Std = binary:replace(binary:replace(Encoded, <<"-">>, <<"+">>, [global]), <<"_">>, <<"/">>, [global]),
%% Add padding if needed
Padded = case byte_size(B64_Std) rem 4 of
0 -> B64_Std;
2 -> <<B64_Std/binary, "==">>;
3 -> <<B64_Std/binary, "=">>
end,
{ok, base64:decode(Padded)}
catch
_:_ -> {error, invalid_base64}
end.
%% @private BLAKE3 hash - pure Erlang implementation
%% This is a simplified BLAKE3 that uses the core algorithm principles
%% but may not produce identical output to the reference implementation.
%% For production, the NIF should be used.
erlang_blake3(Data) ->
%% BLAKE3 IV (same as BLAKE2s)
IV = {16#6A09E667, 16#BB67AE85, 16#3C6EF372, 16#A54FF53A,
16#510E527F, 16#9B05688C, 16#1F83D9AB, 16#5BE0CD19},
ChunkLen = 1024,
Chunks = blake3_chunk_data(Data, ChunkLen),
ChunkCount = length(Chunks),
ChunkHashes = [blake3_compress_chunk(IV, C, I, I =:= 0, I =:= ChunkCount - 1, ChunkCount =:= 1)
|| {I, C} <- lists:zip(lists:seq(0, ChunkCount - 1), Chunks)],
blake3_finalize(ChunkHashes, IV).
%% @private Split data into chunks
blake3_chunk_data(<<>>, _ChunkLen) -> [<<>>];
blake3_chunk_data(Data, ChunkLen) -> blake3_chunk_data(Data, ChunkLen, []).
blake3_chunk_data(<<>>, _ChunkLen, Acc) -> lists:reverse(Acc);
blake3_chunk_data(Data, ChunkLen, Acc) when byte_size(Data) =< ChunkLen ->
lists:reverse([Data | Acc]);
blake3_chunk_data(Data, ChunkLen, Acc) ->
<<Chunk:ChunkLen/binary, Rest/binary>> = Data,
blake3_chunk_data(Rest, ChunkLen, [Chunk | Acc]).
%% @private Compress a chunk
blake3_compress_chunk(IV, Chunk, _ChunkIdx, _IsFirst, _IsLast, _IsSingle) ->
%% Simplified compression using SHA-256 core (not true BLAKE3)
%% Real BLAKE3 uses a different compression function
<<Hash:32/binary, _/binary>> = crypto:hash(sha256, <<(element(1, IV)):32, Chunk/binary>>),
Hash.
%% @private Finalize tree hash
blake3_finalize([Hash], _IV) -> Hash;
blake3_finalize(Hashes, IV) ->
Pairs = blake3_pair_hashes(Hashes),
NewHashes = [crypto:hash(sha256, <<L/binary, R/binary>>) || {L, R} <- Pairs],
blake3_finalize(NewHashes, IV).
blake3_pair_hashes([]) -> [];
blake3_pair_hashes([H]) -> [{H, <<0:256>>}];
blake3_pair_hashes([H1, H2 | Rest]) -> [{H1, H2} | blake3_pair_hashes(Rest)].
%% @private Hex encode binary
hex_encode(Bin) ->
<< <<(hex_digit(N))>> || <<N:4>> <= Bin >>.
hex_digit(N) when N < 10 -> $0 + N;
hex_digit(N) -> $a + N - 10.
%% @private Constant-time comparison
erlang_secure_compare(A, B) when byte_size(A) =/= byte_size(B) ->
false;
erlang_secure_compare(A, B) ->
%% Constant-time XOR comparison
AList = binary_to_list(A),
BList = binary_to_list(B),
Result = lists:foldl(fun({X, Y}, Acc) -> Acc bor (X bxor Y) end, 0, lists:zip(AList, BList)),
Result =:= 0.