Current section
Files
Jump to
Current section
Files
src/hypersig/digest.gleam
// SPDX-FileCopyrightText: 2025 Emerald <emerald@emeraldgreen.dev>
//
// SPDX-License-Identifier: GPL-3.0-or-later
import gleam/bit_array
import gleam/crypto
import gleam/list
import gleam/result
import gleam/string
import hypersig/error
pub type DigestHeader =
List(Digest)
pub type Digest {
Digest(method: crypto.HashAlgorithm, digest: BitArray)
}
fn method_to_string(alg: crypto.HashAlgorithm) {
case alg {
crypto.Md5 -> "md5"
crypto.Sha1 -> "sha-1"
crypto.Sha224 -> "sha-224"
crypto.Sha256 -> "sha-256"
crypto.Sha384 -> "sha-384"
crypto.Sha512 -> "sha-512"
}
}
fn method_from_string(
alg: String,
) -> Result(crypto.HashAlgorithm, error.HyperError) {
case alg {
"md5" -> Ok(crypto.Md5)
"sha-1" -> Ok(crypto.Sha1)
"sha-224" -> Ok(crypto.Sha224)
"sha-256" -> Ok(crypto.Sha256)
"sha-384" -> Ok(crypto.Sha384)
"sha-512" -> Ok(crypto.Sha512)
_ -> Error(error.UnsupportedDigest)
}
}
/// Format a Digest as `<method>=<base64 encoded digest>`
pub fn to_string(header: Digest) -> String {
method_to_string(header.method)
<> "="
<> bit_array.base64_url_encode(header.digest, True)
}
/// Parse a Digest from a string
pub fn from_string(header: String) -> Result(Digest, error.HyperError) {
use #(method, digest) <- result.try(
string.split_once(header, on: "=")
|> result.replace_error(error.DigestParse),
)
use method <- result.try(method_from_string(method))
use digest <- result.try(
bit_array.base64_url_decode(digest)
|> result.replace_error(error.DigestParse),
)
Ok(Digest(method:, digest:))
}
/// Hashes the data using Sha256 and creates a Digest object
pub fn new(data: BitArray) -> Digest {
with_method(crypto.Sha256, data)
}
/// Hashes the data with the given method and creates a Digest object
pub fn with_method(method, data: BitArray) {
let digest = crypto.hash(method, data)
Digest(method:, digest:)
}
/// Verifies the given digest object matches the given data
pub fn verify(digest: Digest, data: BitArray) -> Bool {
let other = with_method(digest.method, data)
other == digest
}
/// Parses a comma-separated list of digest strings into a list of Digest objects
pub fn parse_header(header: String) -> Result(DigestHeader, error.HyperError) {
string.split(header, on: ",")
|> list.map(from_string)
|> result.all()
|> result.replace_error(error.DigestParse)
}
/// Verify all digests in a given header against the given data
pub fn verify_header(header: DigestHeader, data: BitArray) -> Bool {
list.all(in: header, satisfying: verify(_, data))
}