hb_crypto.erl - Cryptographic Primitives & HashPath Algorithms
Overview
Purpose: Cryptographic functions and hash chain algorithms for HyperBEAM
Module: hb_crypto
Algorithms: SHA-256 chaining, accumulation-based commitment
Safety: Carefully managed cryptographic primitives wrapper
This module implements the cryptographic foundations of HyperBEAM, providing hash chain algorithms for message resolution tracking and secure commitment generation. It wraps dangerous low-level crypto operations with type-safe interfaces.
HashPath Algorithms
HyperBEAM implements two distinct hashpath algorithms:
sha-256-chain: Sequential SHA-256 hash chaining that preserves orderaccumulate-256: Order-independent SHA-256 accumulation for commitment
The accumulate algorithm is experimental and exists to test multiple HashPath strategies.
Dependencies
- Erlang/OTP:
crypto(OpenSSL backend) - Includes:
include/hb.hrl
Public Functions Overview
%% Hash Chain Operations
-spec sha256(Data) -> Hash.
-spec sha256_chain(ID1, ID2) -> ChainedHash.
%% Accumulation Operations
-spec accumulate(IDs) -> Commitment.
-spec accumulate(ID1, ID2) -> Commitment.
%% Key Derivation
-spec pbkdf2(Alg, Password, Salt, Iterations, KeyLength) -> {ok, Key} | {error, Reason}.Public Functions
1. sha256/1
-spec sha256(Data) -> Hash
when
Data :: binary() | iolist(),
Hash :: binary().Description: Compute SHA-256 hash of data. Wraps Erlang's crypto:hash/2 with OpenSSL backend.
-module(hb_crypto_sha256_test).
-include_lib("eunit/include/eunit.hrl").
sha256_basic_test() ->
Data = <<"hello world">>,
Hash = hb_crypto:sha256(Data),
?assert(is_binary(Hash)),
?assertEqual(32, byte_size(Hash)).
sha256_deterministic_test() ->
Data = <<"test data">>,
Hash1 = hb_crypto:sha256(Data),
Hash2 = hb_crypto:sha256(Data),
?assertEqual(Hash1, Hash2).
sha256_empty_test() ->
Hash = hb_crypto:sha256(<<>>),
?assert(is_binary(Hash)),
?assertEqual(32, byte_size(Hash)).
sha256_iolist_test() ->
Data = [<<"hello">>, <<" ">>, <<"world">>],
Hash = hb_crypto:sha256(Data),
?assert(is_binary(Hash)),
?assertEqual(32, byte_size(Hash)).2. sha256_chain/2
-spec sha256_chain(ID1, ID2) -> ChainedHash
when
ID1 :: binary(),
ID2 :: binary(),
ChainedHash :: binary().Description: Chain two IDs together using SHA-256. ID1 must be exactly 32 bytes. Creates a hash of the concatenation: SHA256(ID1 || ID2). Preserves ordering information.
-module(hb_crypto_sha256_chain_test).
-include_lib("eunit/include/eunit.hrl").
sha256_chain_basic_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Hash = hb_crypto:sha256_chain(ID1, ID2),
?assert(is_binary(Hash)),
?assertEqual(32, byte_size(Hash)).
sha256_chain_matches_hash_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
ChainHash = hb_crypto:sha256_chain(ID1, ID2),
DirectHash = crypto:hash(sha256, <<ID1/binary, ID2/binary>>),
?assertEqual(ChainHash, DirectHash).
sha256_chain_order_matters_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Hash1 = hb_crypto:sha256_chain(ID1, ID2),
Hash2 = hb_crypto:sha256_chain(ID2, ID1),
?assertNotEqual(Hash1, Hash2).
sha256_chain_entropy_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Hash = hb_crypto:sha256_chain(ID1, ID2),
% Basic entropy check - count zeros
Zeroes = count_leading_zeroes(Hash),
Avg = Zeroes / 256,
?assert(Avg > 0.4),
?assert(Avg < 0.6).
sha256_chain_bad_id1_throws_test() ->
ID1 = <<1:128>>, % Too short
ID2 = <<2:256>>,
?assertThrow({cannot_chain_bad_ids, _, _},
hb_crypto:sha256_chain(ID1, ID2)).
count_leading_zeroes(Bin) ->
count_zeroes(Bin, 0).
count_zeroes(<<>>, Acc) -> Acc;
count_zeroes(<<0:1, Rest/bitstring>>, Acc) ->
count_zeroes(Rest, Acc + 1);
count_zeroes(<<_:1, Rest/bitstring>>, Acc) ->
count_zeroes(Rest, Acc).3. accumulate/1, accumulate/2
-spec accumulate(IDs) -> Commitment
when
IDs :: [binary()],
Commitment :: binary().
-spec accumulate(ID1, ID2) -> Commitment
when
ID1 :: binary(),
ID2 :: binary(),
Commitment :: binary().Description: Accumulate cryptographically-secure 256-bit IDs into a single commitment by addition. No ordering information is preserved. Both IDs must be exactly 32 bytes. For lists, folds over IDs starting from <<0:256>>.
-module(hb_crypto_accumulate_test).
-include_lib("eunit/include/eunit.hrl").
accumulate_basic_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Result = hb_crypto:accumulate(ID1, ID2),
?assert(is_binary(Result)),
?assertEqual(32, byte_size(Result)).
accumulate_addition_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
<<Result:256>> = hb_crypto:accumulate(ID1, ID2),
?assertEqual(3, Result).
accumulate_order_independent_test() ->
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Result1 = hb_crypto:accumulate(ID1, ID2),
Result2 = hb_crypto:accumulate(ID2, ID1),
?assertEqual(Result1, Result2).
accumulate_list_test() ->
IDs = [<<1:256>>, <<2:256>>, <<3:256>>],
Result = hb_crypto:accumulate(IDs),
?assert(is_binary(Result)),
?assertEqual(32, byte_size(Result)),
<<ResultInt:256>> = Result,
?assertEqual(6, ResultInt).
accumulate_empty_list_test() ->
Result = hb_crypto:accumulate([]),
?assertEqual(<<0:256>>, Result).
accumulate_single_test() ->
ID = <<5:256>>,
Result = hb_crypto:accumulate([ID]),
?assertEqual(ID, Result).
accumulate_bad_id_throws_test() ->
ID1 = <<1:128>>, % Too short
ID2 = <<2:256>>,
?assertThrow({cannot_accumulate_bad_ids, _, _},
hb_crypto:accumulate(ID1, ID2)).
accumulate_large_values_test() ->
% Test with large values to ensure proper overflow handling
ID1 = <<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:256>>,
ID2 = <<1:256>>,
Result = hb_crypto:accumulate(ID1, ID2),
?assert(is_binary(Result)),
?assertEqual(32, byte_size(Result)),
% Result should overflow and wrap
?assertEqual(<<0:256>>, Result).4. pbkdf2/5
-spec pbkdf2(Alg, Password, Salt, Iterations, KeyLength) -> {ok, Key} | {error, Reason}
when
Alg :: atom(),
Password :: binary(),
Salt :: binary(),
Iterations :: integer(),
KeyLength :: integer(),
Key :: binary(),
Reason :: term().Description: Password-Based Key Derivation Function 2 (PBKDF2) wrapper. Uses HMAC with specified algorithm to derive a key from a password.
Test Code:-module(hb_crypto_pbkdf2_test).
-include_lib("eunit/include/eunit.hrl").
pbkdf2_basic_test() ->
Password = <<"password">>,
Salt = <<"salt">>,
Iterations = 1000,
KeyLength = 32,
{ok, Key} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, KeyLength),
?assert(is_binary(Key)),
?assertEqual(KeyLength, byte_size(Key)).
pbkdf2_deterministic_test() ->
Password = <<"password">>,
Salt = <<"salt">>,
Iterations = 1000,
KeyLength = 32,
{ok, Key1} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, KeyLength),
{ok, Key2} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, KeyLength),
?assertEqual(Key1, Key2).
pbkdf2_different_passwords_test() ->
Password1 = <<"password1">>,
Password2 = <<"password2">>,
Salt = <<"salt">>,
Iterations = 1000,
KeyLength = 32,
{ok, Key1} = hb_crypto:pbkdf2(sha256, Password1, Salt, Iterations, KeyLength),
{ok, Key2} = hb_crypto:pbkdf2(sha256, Password2, Salt, Iterations, KeyLength),
?assertNotEqual(Key1, Key2).
pbkdf2_different_salts_test() ->
Password = <<"password">>,
Salt1 = <<"salt1">>,
Salt2 = <<"salt2">>,
Iterations = 1000,
KeyLength = 32,
{ok, Key1} = hb_crypto:pbkdf2(sha256, Password, Salt1, Iterations, KeyLength),
{ok, Key2} = hb_crypto:pbkdf2(sha256, Password, Salt2, Iterations, KeyLength),
?assertNotEqual(Key1, Key2).
pbkdf2_variable_length_test() ->
Password = <<"password">>,
Salt = <<"salt">>,
Iterations = 1000,
{ok, Key16} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, 16),
{ok, Key32} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, 32),
{ok, Key64} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, 64),
?assertEqual(16, byte_size(Key16)),
?assertEqual(32, byte_size(Key32)),
?assertEqual(64, byte_size(Key64)).
pbkdf2_different_algorithms_test() ->
Password = <<"password">>,
Salt = <<"salt">>,
Iterations = 1000,
KeyLength = 32,
{ok, KeySHA256} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, KeyLength),
{ok, KeySHA512} = hb_crypto:pbkdf2(sha512, Password, Salt, Iterations, KeyLength),
?assertNotEqual(KeySHA256, KeySHA512).Common Patterns
%% Hash data
Data = <<"important data">>,
Hash = hb_crypto:sha256(Data).
%% Chain IDs to track computation
ID1 = <<1:256>>,
ID2 = <<2:256>>,
ChainedID = hb_crypto:sha256_chain(ID1, ID2),
ID3 = <<3:256>>,
FinalChain = hb_crypto:sha256_chain(ChainedID, ID3).
%% Accumulate IDs (order-independent)
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Commitment = hb_crypto:accumulate(ID1, ID2).
%% Accumulate list of IDs
IDs = [<<1:256>>, <<2:256>>, <<3:256>>],
TotalCommitment = hb_crypto:accumulate(IDs).
%% Derive encryption key from password
Password = <<"user_password">>,
Salt = crypto:strong_rand_bytes(16),
Iterations = 100000,
KeyLength = 32,
{ok, EncryptionKey} = hb_crypto:pbkdf2(sha256, Password, Salt, Iterations, KeyLength).
%% Use in message resolution tracking
BaseID = hb_crypto:sha256(Message),
Step1ID = hb_crypto:sha256_chain(BaseID, Resolution1),
Step2ID = hb_crypto:sha256_chain(Step1ID, Resolution2).HashPath Comparison
SHA-256 Chain
Properties:- Order-dependent
- Sequential processing
- Preserves computation sequence
- Used for message resolution tracking
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Chain1 = hb_crypto:sha256_chain(ID1, ID2), % Hash(ID1 || ID2)
Chain2 = hb_crypto:sha256_chain(ID2, ID1), % Hash(ID2 || ID1)
% Chain1 =/= Chain2 (order matters)Accumulate-256
Properties:- Order-independent
- Parallel processing friendly
- No sequence information
- Experimental commitment scheme
ID1 = <<1:256>>,
ID2 = <<2:256>>,
Acc1 = hb_crypto:accumulate(ID1, ID2), % ID1 + ID2
Acc2 = hb_crypto:accumulate(ID2, ID1), % ID2 + ID1
% Acc1 == Acc2 (order doesn't matter)Security Considerations
ID Requirements
Both chain and accumulate functions require cryptographically-secure 256-bit IDs:
- Must be exactly 32 bytes (256 bits)
- Should be generated from secure hash functions
- Must have high entropy
Invalid Input Handling
%% Bad ID1 in chain - throws exception
BadID = <<1:128>>,
GoodID = <<2:256>>,
hb_crypto:sha256_chain(BadID, GoodID).
% Throws: {cannot_chain_bad_ids, BadID, GoodID}
%% Bad ID in accumulate - throws exception
hb_crypto:accumulate(BadID, GoodID).
% Throws: {cannot_accumulate_bad_ids, BadID, GoodID}PBKDF2 Recommendations
- Iterations: Use at least 100,000 for new applications
- Salt: Always use unique, random salt per password
- Key Length: Match your encryption algorithm requirements
- Algorithm: SHA-256 minimum, SHA-512 preferred for high security
References
- OpenSSL Crypto - Erlang
cryptomodule backend - SHA-256 Specification - FIPS 180-4
- PBKDF2 Specification - RFC 2898
- HyperBEAM HashPath - Message resolution tracking
- AO-Core Protocol - Cryptographic message chaining
Notes
- OpenSSL Backend: All operations use Erlang's
cryptomodule backed by OpenSSL - Type Safety: Functions throw on invalid input to prevent silent failures
- ID Length: Strict 32-byte requirement for chain and accumulate operations
- Entropy: Output has approximately 50% bit distribution (high entropy)
- Experimental: Accumulate algorithm is experimental and may change
- Order Independence: Accumulate useful for parallel message processing
- Overflow Handling: Accumulate wraps on overflow (256-bit addition)
- Performance: SHA-256 chain is the production-ready algorithm
- PBKDF2 Errors: Returns
{error, Reason}instead of throwing - Process Dictionary: No state stored, all functions pure and thread-safe