///|
/// Hash algorithms used by OpenPGP, identified by their RFC 9580 ยง9.5
/// algorithm numbers. This is the PGP-level identity; concrete digest
/// computation is delegated to `moonbitlang/x/crypto` inside this package
/// only, so the hash backend can be swapped without touching callers.
pub(all) enum HashId {
Md5
Sha1
Ripemd160
Sha224
Sha256
Sha384
Sha512
} derive(Eq, Debug)
///|
pub fn HashId::number(self : HashId) -> Int32 {
match self {
Md5 => 1
Sha1 => 2
Ripemd160 => 3
Sha224 => 11
Sha256 => 8
Sha384 => 9
Sha512 => 10
}
}
///|
pub fn hash_id_of_number(n : Int32) -> HashId? {
match n {
1 => Some(Md5)
2 => Some(Sha1)
3 => Some(Ripemd160)
8 => Some(Sha256)
9 => Some(Sha384)
10 => Some(Sha512)
11 => Some(Sha224)
_ => None
}
}
///|
/// Size of the digest output in bytes.
pub fn HashId::digest_size(self : HashId) -> Int32 {
match self {
Md5 => 16
Sha1 => 20
Ripemd160 => 20
Sha224 => 28
Sha256 => 32
Sha384 => 48
Sha512 => 64
}
}
///|
/// Digest function block size in bytes (as used by HMAC, RFC 2104).
pub fn HashId::block_size(self : HashId) -> Int32 {
match self {
Sha384 | Sha512 => 128
_ => 64
}
}
///|
/// Digest a single buffer.
pub fn HashId::digest(self : HashId, data : BytesView) -> Bytes {
match self {
Md5 => Bytes::from_iter(@xcrypto.md5(data).iter())
Sha1 => Bytes::from_iter(@xcrypto.sha1(data).iter())
Ripemd160 => Bytes::from_iter(@xcrypto.ripemd160(data).iter())
Sha224 => Bytes::from_iter(@xcrypto.sha224(data).iter())
Sha256 => Bytes::from_iter(@xcrypto.sha256(data).iter())
Sha384 => Bytes::from_iter(@xcrypto.sha384(data).iter())
Sha512 => Bytes::from_iter(@xcrypto.sha512(data).iter())
}
}
///|
/// HMAC (RFC 2104) over a single buffer, using this hash algorithm.
pub fn HashId::hmac(self : HashId, key : BytesView, data : BytesView) -> Bytes {
let bs = self.block_size()
let k : Bytes = if key.length() > bs {
self.digest(key)
} else {
key.to_owned()
}
let kp = pad_right(k, bs)
let ipad = Bytes::makei(bs, i => kp[i] ^ b'\x36')
let opad = Bytes::makei(bs, i => kp[i] ^ b'\x5c')
let inner = self.digest(ipad.add(data.to_owned()))
self.digest(opad.add(inner))
}
///|
/// DER DigestInfo prefix for this hash (PKCS#1 v1.5 signatures).
pub fn HashId::der_prefix(self : HashId) -> Bytes {
from_hex_unchecked(
match self {
Md5 => "3020300c06082a864886f70d020505000410"
Sha1 => "3021300906052b0e03021a05000414"
Ripemd160 => "3021300906052b2403020105000414"
Sha224 => "302d300d06096086480165030402040500041c"
Sha256 => "3031300d060960864801650304020105000420"
Sha384 => "3041300d060960864801650304020205000430"
Sha512 => "3051300d060960864801650304020305000440"
},
)
}
///|
/// Hex decode of a compile-time constant; invalid constants abort.
fn from_hex_unchecked(s : String) -> Bytes {
match hex_decode_const(s) {
Some(b) => b
None => abort("primitives: invalid constant hex string")
}
}
///|
fn hex_decode_const(s : String) -> Bytes? {
Some(@hex.decode(s) catch { _ => return None })
}
///|
pub extend HashId with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend HashId with Eq::{not_equal, equal}
///|
/// One streaming hash instance over the algorithms that expose one
/// (SHA-1/SHA-384 have no streaming form and take the materialize
/// fallback in `hash_repetition`).
priv enum Hasher {
Sha256(@xcrypto.SHA256)
Sha512(@xcrypto.SHA512)
Md5(@xcrypto.MD5)
Sha224(@xcrypto.SHA224)
Ripemd160(@xcrypto.RIPEMD160)
}
///|
fn hasher_of(id : HashId) -> Hasher? {
match id {
Sha256 => Some(Hasher::Sha256(@xcrypto.SHA256::new()))
Sha512 => Some(Hasher::Sha512(@xcrypto.SHA512::new()))
Md5 => Some(Hasher::Md5(@xcrypto.MD5::new()))
Sha224 => Some(Hasher::Sha224(@xcrypto.SHA224::new()))
Ripemd160 => Some(Hasher::Ripemd160(@xcrypto.RIPEMD160::new()))
Sha1 | Sha384 => None
}
}
///|
fn hasher_update(h : Hasher, data : FixedArray[Byte]) -> Unit {
match h {
Sha256(s) => s.update(data)
Sha512(s) => s.update(data)
Md5(s) => s.update(data)
Sha224(s) => s.update(data)
Ripemd160(s) => s.update(data)
}
}
///|
fn hasher_final(h : Hasher) -> Bytes {
let out : FixedArray[Byte] = match h {
Sha256(s) => s.finalize()
Sha512(s) => s.finalize()
Md5(s) => s.finalize()
Sha224(s) => s.finalize()
Ripemd160(s) => s.finalize()
}
Bytes::from_iter(out.iter())
}
///|
/// Digest `prefix || block*reps || rem` without materializing the
/// repetition: the iterated S2K's octet count is ~54 MiB at gpg's default,
/// and building that byte string dominated derivation time (measured 4s
/// per derivation dropped materially once the allocation went away).
/// SHA-1 and SHA-384 have no streaming form here and fall back to a
/// materializing digest.
pub fn hash_repetition(
id : HashId,
prefix : BytesView,
block : BytesView,
reps : Int32,
rem : BytesView,
) -> Bytes raise {
guard block.length() > 0 else {
@error.fail("primitives: hash_repetition needs a non-empty block")
}
guard reps >= 0 else { @error.fail("primitives: negative repetition") }
match hasher_of(id) {
Some(h) => {
hasher_update(h, FixedArray::from_iter(prefix.iter()))
let block_fa = FixedArray::from_iter(block.iter())
let rem_fa = FixedArray::from_iter(rem.iter())
for _ in 0.. {
// Fallback: build the byte string once and digest it whole.
let out : Array[Byte] = Array(
capacity=prefix.length() + block.length() * reps + rem.length(),
)
for b in prefix {
out.push(b)
}
for _ in 0..