Packages

A replacement for gleam_crypto that works on all targets.

Current section

Files

Jump to
munch src munch.gleam
Raw

src/munch.gleam

//// Cryptographic hash functions and related utilities.
////
//// <style>
//// small {
//// font-size: 0.7em;
//// opacity: 0.75;
//// }
//// .goto-top {
//// display: block;
//// font-size: 0.85em;
//// text-align: right;
//// }
//// h4 {
//// margin-bottom: 0;
//// + p {
//// margin-top: 0;
//// }
//// }
//// </style>
//// <script>
//// (callback => document.readyState !== 'loading' ? callback() : document.addEventListener('DOMContentLoaded', callback, { once: true }))(() => {
//// const list = document.querySelector('.sidebar > ul:last-of-type')
//// const sortedLists = document.createDocumentFragment()
//// const sortedMembers = document.createDocumentFragment()
////
//// for (const header of document.querySelectorAll('main > h4')) {
//// sortedLists.append((() => {
//// const node = document.createElement('h3')
//// node.append(header.textContent)
//// return node
//// })())
//// sortedMembers.append((() => {
//// const node = document.createElement('h2')
//// node.append(header.textContent)
//// return node
//// })())
////
//// const sortedList = document.createElement('ul')
//// sortedLists.append(sortedList)
////
//// for (const anchor of header.nextElementSibling.querySelectorAll('a')) {
//// const href = anchor.getAttribute('href')
//// const member = document.querySelector(`.member:has(h2 > a[href="${href}"])`)
//// const sidebar = list.querySelector(`li:has(a[href="${href}"])`)
//// sortedList.append(sidebar)
//// sortedMembers.append(member)
//// }
//// }
////
//// document.querySelector('.sidebar').insertBefore(sortedLists, list)
//// document.querySelector('.module-members:has(#module-values)').insertBefore(sortedMembers, document.querySelector('#module-values').nextSibling)
////
//// const goToTop = document.createElement('a')
//// goToTop.classList.add('goto-top')
//// goToTop.setAttribute('href', '#')
//// goToTop.textContent = 'Back to top ↑'
//// for (const member of document.querySelectorAll('.member')) {
//// member.insertBefore(goToTop.cloneNode(true), null)
//// }
//// })
//// </script>
////
//// #### Hash Algorithms
//// [md5](#md5 "The MD5 hash algorithm"),
//// [sha1](#sha1 "The SHA-1 hash algorithm"),
//// [sha224](#sha224 "The SHA-224 hash algorithm"),
//// [sha256](#sha256 "The SHA-256 hash algorithm"),
//// [sha384](#sha384 "The SHA-384 hash algorithm"),
//// [sha512](#sha512 "The SHA-512 hash algorithm")
////
//// #### Block API
//// [hash](#hash "Hash a UTF-8 encoded string"),
//// [hash_bits](#hash_bits "Hash a bit string"),
//// [hmac](#hmac "Authenticate a UTF-8 encoded string"),
//// [hmac_bits](#hmac_bits "Authenticate a bit string")
////
//// #### Stream API
//// [new_hash](#new_hash "Start a streaming hash"),
//// [new_hmac](#new_hmac "Start a streaming HMAC"),
//// [update](#update "Add a UTF-8 encoded string"),
//// [update_bits](#update_bits "Add binary data"),
//// [digest](#digest "Return the base16 encoded digest"),
//// [digest_bits](#digest_bits "Return the binary digest")
////
//// #### Utils
//// [secure_compare](#secure_compare "Compare two binaries in constant time"),
//// [sign_message](#sign_message "Sign a message"),
//// [verify_signed_message](#verify_signed_message "Verify a signed message"),
//// [strong_random_bytes](#strong_random_bytes "Generate cryptographically secure random bytes")
import gleam/bit_array
import gleam/result
import gleam/string
import munch/internal/bitwise.{bor, xor}
import munch/internal/hash
import munch/internal/hmac
import munch/internal/md5
import munch/internal/native
import munch/internal/sha1
import munch/internal/sha256
import munch/internal/sha512
// -- TYPES -------------------------------------------------------------------
/// The state of a streaming hash or HMAC calculation.
///
pub type Hash =
hash.Hash
/// A hash algorithm.
///
/// Pass one of the algorithm values defined in this module, such as `sha256`, to
/// functions which accept a `HashAlgorithm`.
///
pub opaque type HashAlgorithm {
HashAlgorithm(init: fn() -> Hash, block_size: Int, signing_name: String)
}
// -- HASH --------------------------------------------------------------------
/// The MD5 hash algorithm.
///
/// MD5 is considered weak and should not be used for security purposes. It may
/// still be useful for non-security purposes or for compatibility with existing
/// systems.
///
pub const md5 = HashAlgorithm(
init: init_md5,
block_size: 64,
signing_name: "HMD5",
)
fn init_md5() -> Hash {
result.lazy_unwrap(native.md5(), md5.new)
}
/// The SHA-1 hash algorithm.
///
/// SHA-1 is considered weak and should not be used for security purposes. It
/// may still be useful for non-security purposes or for compatibility with
/// existing systems.
///
pub const sha1 = HashAlgorithm(
init: init_sha1,
block_size: 64,
signing_name: "HS1",
)
fn init_sha1() -> Hash {
result.lazy_unwrap(native.sha1(), sha1.new)
}
/// The SHA-224 hash algorithm.
///
pub const sha224 = HashAlgorithm(
init: init_sha224,
block_size: 64,
signing_name: "HS224",
)
fn init_sha224() -> Hash {
result.lazy_unwrap(native.sha224(), sha256.new224)
}
/// The SHA-256 hash algorithm.
///
pub const sha256 = HashAlgorithm(
init: init_sha256,
block_size: 64,
signing_name: "HS256",
)
fn init_sha256() -> Hash {
result.lazy_unwrap(native.sha256(), sha256.new)
}
/// The SHA-384 hash algorithm.
///
pub const sha384 = HashAlgorithm(
init: init_sha384,
block_size: 128,
signing_name: "HS384",
)
fn init_sha384() -> Hash {
result.lazy_unwrap(native.sha384(), sha512.new384)
}
/// The SHA-512 hash algorithm.
///
pub const sha512 = HashAlgorithm(
init: init_sha512,
block_size: 128,
signing_name: "HS512",
)
fn init_sha512() -> Hash {
result.lazy_unwrap(native.sha512(), sha512.new)
}
// -- BLOCK API ---------------------------------------------------------------
/// Computes the base16 encoded digest of a UTF-8 encoded string.
///
/// ## Examples
///
/// ```gleam
/// import munch
///
/// assert munch.hash(munch.sha256, "a")
/// == "CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB"
/// ```
///
/// To hash content in multiple chunks, see the `new_hash` function.
///
pub fn hash(algorithm: HashAlgorithm, string: String) -> String {
algorithm
|> new_hash
|> update(string)
|> digest
}
/// Computes the binary digest of a bit string.
///
/// ## Returns
///
/// The digest as a `BitArray`.
///
/// ## Examples
///
/// ```gleam
/// import gleam/bit_array
/// import munch
///
/// assert munch.hash_bits(munch.sha256, <<"a":utf8>>)
/// |> bit_array.base16_encode
/// == "CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB"
/// ```
///
/// To hash content in multiple chunks, see the `new_hash` function.
///
pub fn hash_bits(algorithm: HashAlgorithm, bits: BitArray) -> BitArray {
algorithm
|> new_hash
|> update_bits(bits)
|> digest_bits
}
/// Computes the base16 encoded HMAC of UTF-8 encoded data using a UTF-8
/// encoded key.
///
/// ## Examples
///
/// ```gleam
/// import munch
///
/// assert munch.hmac("message", munch.sha256, "key")
/// == "6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A"
/// ```
///
/// To authenticate content in multiple chunks, see the `new_hmac` function.
///
pub fn hmac(data: String, algorithm: HashAlgorithm, key: String) -> String {
algorithm
|> new_hmac(<<key:utf8>>)
|> update(data)
|> digest
}
/// Computes the binary HMAC of a bit string.
///
/// If the key does not contain a whole number of bytes, its final byte is
/// padded with trailing zero bits before use.
///
/// ## Examples
///
/// ```gleam
/// import gleam/bit_array
/// import munch
///
/// assert munch.hmac_bits(<<"message":utf8>>, munch.sha256, <<"key":utf8>>)
/// |> bit_array.base16_encode
/// == "6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A"
/// ```
///
/// To authenticate content in multiple chunks, see the `new_hmac` function.
///
pub fn hmac_bits(
data: BitArray,
algorithm: HashAlgorithm,
key: BitArray,
) -> BitArray {
algorithm
|> new_hmac(key)
|> update_bits(data)
|> digest_bits
}
// -- STREAM API --------------------------------------------------------------
/// Initializes the state for a streaming hash digest calculation.
///
/// Add data using `update_bits` or `update`, then retrieve the digest using
/// `digest_bits` or `digest`.
///
/// This is useful for hashing streams or large amounts of data without loading
/// all of it into memory at once.
///
/// ## Examples
///
/// ```gleam
/// import munch
///
/// let digest =
/// munch.new_hash(munch.sha512)
/// |> munch.update("data to hash")
/// |> munch.update("more data")
/// |> munch.digest
/// ```
///
pub fn new_hash(algorithm: HashAlgorithm) -> Hash {
algorithm.init()
}
/// Initializes a streaming HMAC (hash-based message authentication code).
///
/// Add data using `update_bits` or `update`, then retrieve the authentication
/// code using `digest_bits` or `digest`.
///
/// If the key does not contain a whole number of bytes, its final byte is
/// padded with trailing zero bits before use.
///
pub fn new_hmac(algorithm: HashAlgorithm, key: BitArray) -> Hash {
let HashAlgorithm(init:, block_size:, ..) = algorithm
hmac.new(init, block_size, key)
}
/// Adds binary data to a streaming hash or HMAC calculation.
///
/// See `new_hash` and `new_hmac` for more information and examples.
///
pub fn update_bits(hash: Hash, bits: BitArray) -> Hash {
hash.update(hash, bits)
}
/// Adds a UTF-8 encoded string to a streaming hash or HMAC calculation.
///
/// See `new_hash` and `new_hmac` for more information and examples.
///
pub fn update(hash: Hash, str: String) -> Hash {
hash.update(hash, <<str:utf8>>)
}
/// Finalizes a streaming hash or HMAC calculation and returns its binary
/// digest.
///
/// See `new_hash` and `new_hmac` for more information and examples.
///
pub fn digest_bits(hash: Hash) -> BitArray {
hash.finish(hash)
}
/// Finalizes a streaming hash or HMAC calculation and returns its base16
/// encoded digest.
///
/// See `new_hash` and `new_hmac` for more information and examples.
///
pub fn digest(hash: Hash) -> String {
bit_array.base16_encode(hash.finish(hash))
}
// -- SIGN AND VERIFY ---------------------------------------------------------
/// Compares two binaries in constant-time to avoid timing attacks.
///
/// For more details see: http://codahale.com/a-lesson-in-timing-attacks/
///
pub fn secure_compare(left: BitArray, right: BitArray) -> Bool {
case bit_array.byte_size(left) == bit_array.byte_size(right) {
True -> do_secure_compare(left, right)
False -> False
}
}
@external(erlang, "crypto", "hash_equals")
fn do_secure_compare(left: BitArray, right: BitArray) -> Bool {
secure_compare_loop(left, right, 0)
}
fn secure_compare_loop(
left: BitArray,
right: BitArray,
accumulator: Int,
) -> Bool {
case left, right {
<<x, left:bytes>>, <<y, right:bytes>> -> {
let accumulator = bor(accumulator, xor(x, y))
secure_compare_loop(left, right, accumulator)
}
_, _ -> left == right && accumulator == 0
}
}
// Based on https://github.com/elixir-plug/plug_crypto/blob/v1.2.1/lib/plug/crypto/message_verifier.ex#L1
//
/// Signs a message which can later be verified using the
/// `verify_signed_message` function to detect if the message has been tampered
/// with.
///
/// A web application could use this verifier to sign HTTP cookies. The data can
/// be read by the user, but cannot be tampered with.
///
pub fn sign_message(
message: BitArray,
secret: BitArray,
algorithm: HashAlgorithm,
) -> String {
let input = signing_input(algorithm, message)
let signature = hmac_bits(<<input:utf8>>, algorithm, secret)
string.concat([input, ".", bit_array.base64_url_encode(signature, False)])
}
// Based on https://github.com/elixir-plug/plug_crypto/blob/v1.2.1/lib/plug/crypto/message_verifier.ex#L1
//
/// Verifies a message created by the `sign_message` function.
///
/// The hash algorithm must be the same one that was used to sign the message.
///
pub fn verify_signed_message(
message: String,
secret: BitArray,
algorithm: HashAlgorithm,
) -> Result(BitArray, Nil) {
use #(protected, payload, signature) <- result.try(
case string.split(message, on: ".") {
[protected, payload, signature] -> Ok(#(protected, payload, signature))
_ -> Error(Nil)
},
)
let text = string.concat([protected, ".", payload])
use payload <- result.try(bit_array.base64_url_decode(payload))
use signature <- result.try(bit_array.base64_url_decode(signature))
use protected <- result.try(bit_array.base64_url_decode(protected))
let HashAlgorithm(signing_name:, ..) = algorithm
case secure_compare(protected, <<signing_name:utf8>>) {
True -> {
let challenge = hmac_bits(<<text:utf8>>, algorithm, secret)
case secure_compare(challenge, signature) {
True -> Ok(payload)
False -> Error(Nil)
}
}
False -> Error(Nil)
}
}
fn signing_input(algorithm: HashAlgorithm, message: BitArray) -> String {
let HashAlgorithm(signing_name: protected, ..) = algorithm
string.concat([
bit_array.base64_url_encode(<<protected:utf8>>, False),
".",
bit_array.base64_url_encode(message, False),
])
}
// -- RANDOMNESS --------------------------------------------------------------
/// Generates a specified number of bytes randomly uniform 0..255, and returns
/// the result in a binary.
///
/// On Erlang this uses a cryptographically secure PRNG seeded and periodically
/// mixed with operating system provided entropy. By default this is the
/// `RAND_bytes` method from OpenSSL.
/// <https://erlang.org/doc/man/crypto.html#strong_rand_bytes-1>
///
/// On JavaScript the WebCrypto API is used.
///
@external(erlang, "crypto", "strong_rand_bytes")
@external(javascript, "./random.ffi.mjs", "strong_random_bytes")
pub fn strong_random_bytes(length: Int) -> BitArray