ceal 0.1.0

Opportunistic E-Mail Encryption - Stand-Alone Library and Extensions for Lettre
Documentation
#[cfg(feature = "tokio")]
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::{
	proto::{
		op::Query,
		rr::{Name, Record, RecordType}
	},
	recursor::{DnssecConfig, DnssecPolicy, Recursor, RecursorError, RecursorOptions}
};
use log::{debug, error};
use openssl::sha::sha256;
use std::{sync::LazyLock, time::Instant};

#[cfg(feature = "tokio")]
type DnsRuntimeProvider = TokioRuntimeProvider;

static DNS_CLIENT: LazyLock<Recursor<DnsRuntimeProvider>> = LazyLock::new(|| {
	// https://www.iana.org/domains/root/servers
	let roots = [
		// a.root-servers.net
		"2001:503:ba3e::2:30".parse().unwrap(),
		"198.41.0.4".parse().unwrap(),
		// b.root-servers.net
		"2801:1b8:10::b".parse().unwrap(),
		"170.247.170.2".parse().unwrap(),
		// c.root-servers.net
		"2001:500:2::c".parse().unwrap(),
		"192.33.4.12".parse().unwrap(),
		// d.root-servers.net
		"2001:500:2d::d".parse().unwrap(),
		"199.7.91.13".parse().unwrap(),
		// e.root-servers.net belongs to NASA and thus US Gov
		// f.root-servers.net
		"2001:500:2f::f".parse().unwrap(),
		"192.5.5.241".parse().unwrap(),
		// g.root-servers.net belongs to US Gov
		// h.root-servers.net belongs to US Gov
		// i.root-servers.net
		"2001:7fe::53".parse().unwrap(),
		"192.36.148.17".parse().unwrap(),
		// j.root-servers.net
		"2001:503:c27::2:30".parse().unwrap(),
		"192.58.128.30".parse().unwrap(),
		// k.root-servers.net
		"2001:7fd::1".parse().unwrap(),
		"193.0.14.129".parse().unwrap(),
		// l.root-servers.net
		"2001:500:9f::42".parse().unwrap(),
		"199.7.83.42".parse().unwrap(),
		// m.root-servers.net
		"2001:dc3::35".parse().unwrap(),
		"202.12.27.33".parse().unwrap()
	];
	Recursor::new(
		&roots,
		DnssecPolicy::ValidateWithStaticKey(DnssecConfig::default()),
		None,
		RecursorOptions::default(),
		DnsRuntimeProvider::new()
	)
	.expect("Failed to initialise DNS client")
});

/// Query a DNS record and filter-map the result to some bytes.
pub(crate) async fn query_record<F, T>(
	name: &Name,
	ty: RecordType,
	filter_map: F
) -> Result<Vec<T>, RecursorError>
where
	F: Fn(Record) -> Option<T>
{
	let query = Query::query(name.clone(), ty);
	debug!("Querying {ty} DNS record for {name}");
	let instant = Instant::now();
	let res = DNS_CLIENT.resolve(query, instant, true).await;
	let elapsed = instant.elapsed().as_secs_f32();
	match res {
		Ok(msg) => {
			debug!(
				"Received {} DNS records in {elapsed} seconds",
				msg.answers.len()
			);
			Ok(msg.answers.into_iter().filter_map(filter_map).collect())
		},
		Err(err) if err.is_no_records_found() || err.is_nx_domain() => {
			debug!("Received 0 DNS records in {elapsed} seconds");
			Ok(Vec::new())
		},
		Err(err) => {
			error!(
				"Error querying {ty} DNS record for {name} after {elapsed} seconds: {err}"
			);
			Err(err)
		}
	}
}

/// Compute the 224-byte truncated SHA-256 hash.
pub(crate) fn sha256_truncated(bytes: &[u8]) -> String {
	let hash = sha256(bytes);
	let hex = hash
		.into_iter()
		.take(28)
		.map(|byte| format!("{byte:02x}"))
		.collect::<Vec<_>>();
	hex.join("")
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_sha256_truncated() {
		assert_eq!(
			sha256_truncated(b"git"),
			"9a881b9b9f23849475296a8cd768ea1965bc3152df7118e60c145975"
		);
	}
}