//// Cryptographic hash functions and related utilities. //// //// //// //// //// #### 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(<>) |> 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, <>) } /// 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 { <>, <> -> { 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(<>, 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, <>) { True -> { let challenge = hmac_bits(<>, 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(<>, 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. /// /// /// 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