From ea7f16a13422006725dc60b754df513a31dbe9e3 Mon Sep 17 00:00:00 2001 From: Norbert Szydlik Date: Mon, 30 Sep 2024 16:52:17 +0200 Subject: [PATCH] Add documentation and examples. Change hash to use hash_init, hash_update and digest --- src/gleam/crypto.gleam | 90 +++++++++++++++++++++++++++++++----- src/gleam_crypto_ffi.erl | 5 +- src/gleam_crypto_ffi.mjs | 17 ++----- test/gleam/crypto_test.gleam | 36 +++++++-------- 4 files changed, 102 insertions(+), 46 deletions(-) diff --git a/src/gleam/crypto.gleam b/src/gleam/crypto.gleam index e46ae9e..99be23c 100644 --- a/src/gleam/crypto.gleam +++ b/src/gleam/crypto.gleam @@ -34,27 +34,93 @@ pub type HashAlgorithm { Sha1 } -/// Computes a digest of the input bit string. -@external(erlang, "gleam_crypto_ffi", "hash") -@external(javascript, "../gleam_crypto_ffi.mjs", "hash") -pub fn hash(a: HashAlgorithm, b: BitArray) -> BitArray +pub type Hasher -pub type HashState +/// Computes a digest of the input bit string. +/// +/// Example: +/// ```gleam +/// let digest = <>):int>> +/// ``` +/// +pub fn hash(a: HashAlgorithm, b: BitArray) -> BitArray { + hash_init(a) + |> hash_update(b) + |> digest +} -/// Starts hash algorithm for streaming +/// Initializes the state for a streaming hash digest calculation. +/// Then you can add data into the digest algorithm using `hash_update` function. +/// Finally you use `hash_final` to retrieve the digest. +/// +/// It is useful for hashing streams of data or +/// large amount of it without the need to load it all to the memory. +/// +/// Simple example: +/// ```gleam +/// let hash = +/// hash_init(Sha512) +/// |> hash_update(with: <<"data to hash":utf8>>) +/// |> digest +/// ``` +/// +/// Example actor that hashes incoming stream of messages. +/// ```gleam +/// import gleam/erlang/process.{type Subject} +/// import gleam/otp/actor +/// import gleam/crypto +/// +/// pub type Command { +/// Hash(data: BitArray) +/// Finalize(reply_with: Subject(BitArray)) +/// Shutdown +/// } +/// +/// // Receive chunks of message to digest in `Hash` messages. +/// // Responds with digest after receiving Finalize message. +/// fn handle_message( +/// algorithm: crypto.HashAlgorithm, +/// ) -> fn(Command, crypto.Hasher) -> actor.Next(Command, crypto.Hasher) { +/// fn(message: Command, hasher: crypto.Hasher) { +/// case message { +/// Shutdown -> actor.Stop(process.Normal) +/// Hash(hash_chunk) -> actor.continue(crypto.hash_update(hasher, hash_chunk)) +/// Finalize(client) -> { +/// process.send(client, crypto.digest(hasher)) +/// actor.continue(crypto.hash_init(algorithm)) +/// } +/// } +/// } +/// } +/// +/// fn start_actor(algorithm: crypto.HashAlgorithm) { +/// actor.start(crypto.hash_init(algorithm), handle_message(algorithm)) +/// } +/// +/// let assert Ok(my_actor) = start_actor(crypto.Sha256) +/// process.send(my_actor, Hash(<<"chunk1":utf8>>)) +/// // many more message chunks +/// process.send(my_actor, Hash(<<"chunkN":utf8>>)) +/// let digest = process.call(my_actor, Finalize, 1000) +/// ``` +/// @external(erlang, "gleam_crypto_ffi", "hash_init") @external(javascript, "../gleam_crypto_ffi.mjs", "hashInit") -pub fn hash_init(a: HashAlgorithm) -> HashState +pub fn hash_init(algorithm algorithm: HashAlgorithm) -> Hasher -/// Starts hash algorithm for streaming +/// Adds data to a streaming digest calculation. +/// See hash_init for more information and examples. +/// @external(erlang, "crypto", "hash_update") @external(javascript, "../gleam_crypto_ffi.mjs", "hashUpdate") -pub fn hash_update(state: HashState, data: BitArray) -> HashState +pub fn hash_update(hasher hasher: Hasher, with hash_chunk: BitArray) -> Hasher -/// Starts hash algorithm for streaming +/// Finalizes a streaming hash calculation. +/// See hash_init for more information and examples. +/// @external(erlang, "crypto", "hash_final") -@external(javascript, "../gleam_crypto_ffi.mjs", "hashFinal") -pub fn hash_final(state: HashState) -> BitArray +@external(javascript, "../gleam_crypto_ffi.mjs", "digest") +pub fn digest(hasher hasher: Hasher) -> BitArray /// Calculates the HMAC (hash-based message authentication code) for a bit /// string. diff --git a/src/gleam_crypto_ffi.erl b/src/gleam_crypto_ffi.erl index d0e8eb5..e72d4e2 100644 --- a/src/gleam_crypto_ffi.erl +++ b/src/gleam_crypto_ffi.erl @@ -1,5 +1,5 @@ -module(gleam_crypto_ffi). --export([hmac/3, hash/2, hash_init/1]). +-export([hmac/3, hash_init/1]). convert_algorithm(Algorithm) -> case Algorithm of @@ -10,8 +10,5 @@ convert_algorithm(Algorithm) -> hmac(Data, Algorithm, Key) -> crypto:mac(hmac, convert_algorithm(Algorithm), Key, Data). -hash(Algorithm, Data) -> - crypto:hash(convert_algorithm(Algorithm), Data). - hash_init(Algorithm) -> crypto:hash_init(convert_algorithm(Algorithm)). diff --git a/src/gleam_crypto_ffi.mjs b/src/gleam_crypto_ffi.mjs index b4478cb..f56e010 100644 --- a/src/gleam_crypto_ffi.mjs +++ b/src/gleam_crypto_ffi.mjs @@ -40,23 +40,16 @@ export function hmac(data, algorithm, key) { return new BitArray(array); } -export function hash(algorithm, data) { - const hasher = crypto.createHash(algorithmName(algorithm)); - hasher.update(data.buffer); - const array = new Uint8Array(hasher.digest()); - return new BitArray(array); -} - export function hashInit(algorithm) { return crypto.createHash(algorithmName(algorithm)); } -export function hashUpdate(state, data) { - state.update(data.buffer); - return state; +export function hashUpdate(hasher, hashChunk) { + hasher.update(hashChunk.buffer); + return hasher; } -export function hashFinal(state) { - const array = new Uint8Array(state.digest()); +export function digest(hasher) { + const array = new Uint8Array(hasher.digest()); return new BitArray(array); } diff --git a/test/gleam/crypto_test.gleam b/test/gleam/crypto_test.gleam index f9073de..2cec77d 100644 --- a/test/gleam/crypto_test.gleam +++ b/test/gleam/crypto_test.gleam @@ -20,9 +20,9 @@ pub fn hash_sha1_test() { pub fn hash_sha1_stream_test() { crypto.hash_init(crypto.Sha1) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Sha1, <<"hello stream":utf8>>)) } @@ -36,9 +36,9 @@ pub fn hash_sha256_test() { pub fn hash_sha256_stream_test() { crypto.hash_init(crypto.Sha256) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Sha256, <<"hello stream":utf8>>)) } @@ -52,9 +52,9 @@ pub fn hash_sha224_test() { pub fn hash_sha224_stream_test() { crypto.hash_init(crypto.Sha224) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Sha224, <<"hello stream":utf8>>)) } @@ -69,9 +69,9 @@ pub fn hash_sha384_test() { pub fn hash_sha384_stream_test() { crypto.hash_init(crypto.Sha384) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Sha384, <<"hello stream":utf8>>)) } @@ -87,9 +87,9 @@ pub fn hash_sha512_test() { pub fn hash_sha512_stream_test() { crypto.hash_init(crypto.Sha512) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Sha512, <<"hello stream":utf8>>)) } @@ -102,9 +102,9 @@ pub fn hash_md5_test() { pub fn hash_md5_stream_test() { crypto.hash_init(crypto.Md5) - |> crypto.hash_update(<<"hello ":utf8>>) - |> crypto.hash_update(<<"stream":utf8>>) - |> crypto.hash_final + |> crypto.hash_update(with: <<"hello ":utf8>>) + |> crypto.hash_update(with: <<"stream":utf8>>) + |> crypto.digest |> should.equal(crypto.hash(crypto.Md5, <<"hello stream":utf8>>)) }