use std::borrow::Cow;
pub(crate) const TAG_LEN: usize = 32;
pub(crate) const KEY_LEN: usize = 32;
#[cfg(feature = "encryption")]
pub(crate) const AEAD_NONCE_LEN: usize = 24;
#[cfg(feature = "encryption")]
pub(crate) const AEAD_TAG_LEN: usize = 16;
#[cfg(not(any(feature = "mac-blake3", feature = "mac-hmac")))]
compile_error!(
"reconcile: no MAC backend selected. Enable feature `mac-blake3` (default) or `mac-hmac`."
);
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
#[derive(Clone)]
pub(crate) struct ClusterKey([u8; KEY_LEN]);
impl ClusterKey {
pub(crate) fn new(bytes: [u8; KEY_LEN]) -> Self {
ClusterKey(bytes)
}
fn as_bytes(&self) -> &[u8; KEY_LEN] {
&self.0
}
}
pub(crate) struct Tag([u8; TAG_LEN]);
impl Tag {
fn as_bytes(&self) -> &[u8; TAG_LEN] {
&self.0
}
}
pub(crate) struct Payload<'a>(Cow<'a, [u8]>);
impl Payload<'_> {
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.0
}
}
pub(crate) trait Mac {
fn tag(key: &ClusterKey, message: &[u8]) -> Tag;
fn verify(key: &ClusterKey, message: &[u8], tag: &[u8]) -> bool;
}
#[cfg(feature = "mac-blake3")]
pub(crate) struct Blake3Mac;
#[cfg(feature = "mac-blake3")]
impl Mac for Blake3Mac {
fn tag(key: &ClusterKey, message: &[u8]) -> Tag {
Tag(*blake3::keyed_hash(key.as_bytes(), message).as_bytes())
}
fn verify(key: &ClusterKey, message: &[u8], tag: &[u8]) -> bool {
let Ok(tag) = <[u8; TAG_LEN]>::try_from(tag) else {
return false;
};
blake3::keyed_hash(key.as_bytes(), message) == blake3::Hash::from_bytes(tag)
}
}
#[cfg(all(feature = "mac-hmac", not(feature = "mac-blake3")))]
pub(crate) struct HmacSha256Mac;
#[cfg(all(feature = "mac-hmac", not(feature = "mac-blake3")))]
impl Mac for HmacSha256Mac {
fn tag(key: &ClusterKey, message: &[u8]) -> Tag {
use hmac::{Hmac, Mac as _};
let mut mac = Hmac::<sha2::Sha256>::new_from_slice(key.as_bytes())
.expect("HMAC accepts any key length");
mac.update(message);
Tag(mac.finalize().into_bytes().into())
}
fn verify(key: &ClusterKey, message: &[u8], tag: &[u8]) -> bool {
use hmac::{Hmac, Mac as _};
let mut mac = Hmac::<sha2::Sha256>::new_from_slice(key.as_bytes())
.expect("HMAC accepts any key length");
mac.update(message);
mac.verify_slice(tag).is_ok()
}
}
#[cfg(feature = "mac-blake3")]
pub(crate) type ClusterMac = Blake3Mac;
#[cfg(all(feature = "mac-hmac", not(feature = "mac-blake3")))]
pub(crate) type ClusterMac = HmacSha256Mac;
#[derive(Clone)]
pub(crate) enum Authenticator {
Disabled,
Enabled(ClusterKey),
#[cfg(feature = "encryption")]
Encrypted(ClusterKey),
}
impl Authenticator {
pub(crate) fn new(key: Option<[u8; KEY_LEN]>, encrypt: bool) -> Self {
match (key, encrypt) {
(None, _) => Authenticator::Disabled,
(Some(bytes), false) => Authenticator::Enabled(ClusterKey::new(bytes)),
#[cfg(feature = "encryption")]
(Some(bytes), true) => Authenticator::Encrypted(ClusterKey::new(bytes)),
#[cfg(not(feature = "encryption"))]
(Some(_), true) => panic!(
"reconcile: encryption requested but the crate was built without the \
`encryption` feature"
),
}
}
pub(crate) fn is_enabled(&self) -> bool {
!matches!(self, Authenticator::Disabled)
}
pub(crate) fn is_encrypted(&self) -> bool {
#[cfg(feature = "encryption")]
{
matches!(self, Authenticator::Encrypted(_))
}
#[cfg(not(feature = "encryption"))]
{
false
}
}
pub(crate) fn overhead(&self) -> usize {
match self {
Authenticator::Disabled => 0,
Authenticator::Enabled(_) => TAG_LEN,
#[cfg(feature = "encryption")]
Authenticator::Encrypted(_) => AEAD_NONCE_LEN + AEAD_TAG_LEN,
}
}
pub(crate) fn seal(&self, payload: &[u8]) -> Option<Vec<u8>> {
match self {
Authenticator::Disabled => None,
Authenticator::Enabled(key) => {
let tag = ClusterMac::tag(key, payload);
let mut framed = Vec::with_capacity(TAG_LEN + payload.len());
framed.extend_from_slice(tag.as_bytes());
framed.extend_from_slice(payload);
Some(framed)
}
#[cfg(feature = "encryption")]
Authenticator::Encrypted(key) => Some(encryption::seal(key, payload)),
}
}
pub(crate) fn open<'a>(&self, datagram: &'a [u8]) -> Option<Payload<'a>> {
match self {
Authenticator::Disabled => Some(Payload(Cow::Borrowed(datagram))),
Authenticator::Enabled(key) => {
if datagram.len() < TAG_LEN {
return None;
}
let (tag, payload) = datagram.split_at(TAG_LEN);
ClusterMac::verify(key, payload, tag).then_some(Payload(Cow::Borrowed(payload)))
}
#[cfg(feature = "encryption")]
Authenticator::Encrypted(key) => {
encryption::open(key, datagram).map(|plaintext| Payload(Cow::Owned(plaintext)))
}
}
}
}
#[cfg(feature = "encryption")]
mod encryption {
use chacha20poly1305::aead::{Aead, OsRng};
use chacha20poly1305::{AeadCore, Key, KeyInit, XChaCha20Poly1305, XNonce};
use super::{ClusterKey, AEAD_NONCE_LEN, AEAD_TAG_LEN};
fn cipher(key: &ClusterKey) -> XChaCha20Poly1305 {
XChaCha20Poly1305::new(Key::from_slice(key.as_bytes()))
}
pub(super) fn seal(key: &ClusterKey, payload: &[u8]) -> Vec<u8> {
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher(key)
.encrypt(&nonce, payload)
.expect("XChaCha20-Poly1305 encryption of a datagram-sized payload cannot fail");
let mut framed = Vec::with_capacity(AEAD_NONCE_LEN + ciphertext.len());
framed.extend_from_slice(nonce.as_slice());
framed.extend_from_slice(&ciphertext);
framed
}
pub(super) fn open(key: &ClusterKey, datagram: &[u8]) -> Option<Vec<u8>> {
if datagram.len() < AEAD_NONCE_LEN + AEAD_TAG_LEN {
return None;
}
let (nonce, ciphertext) = datagram.split_at(AEAD_NONCE_LEN);
cipher(key)
.decrypt(XNonce::from_slice(nonce), ciphertext)
.ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(byte: u8) -> ClusterKey {
ClusterKey::new([byte; KEY_LEN])
}
#[test]
fn tag_verify_roundtrip() {
let k = key(0x11);
let t = ClusterMac::tag(&k, b"hello world");
assert!(ClusterMac::verify(&k, b"hello world", t.as_bytes()));
}
#[test]
fn tamper_detection() {
let k = key(0x11);
let payload = b"the quick brown fox".to_vec();
let t = ClusterMac::tag(&k, &payload);
let mut bad_payload = payload.clone();
bad_payload[0] ^= 0x01;
assert!(!ClusterMac::verify(&k, &bad_payload, t.as_bytes()));
let mut bad_tag = *t.as_bytes();
bad_tag[0] ^= 0x01;
assert!(!ClusterMac::verify(&k, &payload, &bad_tag));
}
#[test]
fn wrong_key_rejected() {
let t = ClusterMac::tag(&key(0x11), b"payload");
assert!(!ClusterMac::verify(&key(0x22), b"payload", t.as_bytes()));
}
#[test]
fn seal_open_roundtrip() {
let auth = Authenticator::new(Some([0x11; KEY_LEN]), false);
let payload = b"some serialized message";
let sealed = auth.seal(payload).expect("enabled");
assert_eq!(sealed.len(), TAG_LEN + payload.len());
assert_eq!(
auth.open(&sealed).map(|p| p.as_bytes().to_vec()),
Some(payload.to_vec())
);
}
#[test]
fn open_too_short() {
let auth = Authenticator::new(Some([0x11; KEY_LEN]), false);
assert!(auth.open(&[0u8; 10]).is_none());
assert!(auth.open(&[]).is_none());
}
#[test]
fn open_wrong_key() {
let sealed = Authenticator::new(Some([0x11; KEY_LEN]), false)
.seal(b"payload")
.expect("enabled");
assert!(Authenticator::new(Some([0x22; KEY_LEN]), false)
.open(&sealed)
.is_none());
}
#[test]
fn disabled_passes_through_and_does_not_seal() {
let auth = Authenticator::new(None, false);
assert!(!auth.is_enabled());
assert!(!auth.is_encrypted());
assert_eq!(auth.overhead(), 0);
assert!(auth.seal(b"payload").is_none());
assert_eq!(
auth.open(b"raw bytes").map(|p| p.as_bytes().to_vec()),
Some(b"raw bytes".to_vec())
);
}
#[cfg(feature = "encryption")]
mod encryption {
use super::*;
fn encryptor(byte: u8) -> Authenticator {
Authenticator::new(Some([byte; KEY_LEN]), true)
}
#[test]
fn roundtrip_and_overhead() {
let auth = encryptor(0x11);
assert!(auth.is_enabled());
assert!(auth.is_encrypted());
assert_eq!(auth.overhead(), AEAD_NONCE_LEN + AEAD_TAG_LEN);
let payload = b"some serialized message";
let sealed = auth.seal(payload).expect("encrypted");
assert_eq!(sealed.len(), AEAD_NONCE_LEN + payload.len() + AEAD_TAG_LEN);
assert_eq!(
auth.open(&sealed).map(|p| p.as_bytes().to_vec()),
Some(payload.to_vec())
);
}
#[test]
fn ciphertext_hides_plaintext() {
let payload = b"the quick brown fox jumps over the lazy dog";
let sealed = encryptor(0x11).seal(payload).expect("encrypted");
assert!(!sealed
.windows(payload.len())
.any(|window| window == payload));
}
#[test]
fn fresh_nonce_per_datagram() {
let auth = encryptor(0x11);
let payload = b"identical payload";
assert_ne!(
auth.seal(payload).expect("encrypted"),
auth.seal(payload).expect("encrypted")
);
}
#[test]
fn tamper_is_rejected() {
let auth = encryptor(0x11);
let mut sealed = auth.seal(b"payload").expect("encrypted");
let last = sealed.len() - 1;
sealed[last] ^= 0x01;
assert!(auth.open(&sealed).is_none());
}
#[test]
fn wrong_key_is_rejected() {
let sealed = encryptor(0x11).seal(b"payload").expect("encrypted");
assert!(encryptor(0x22).open(&sealed).is_none());
}
#[test]
fn truncated_is_rejected() {
let auth = encryptor(0x11);
assert!(auth
.open(&[0u8; AEAD_NONCE_LEN + AEAD_TAG_LEN - 1])
.is_none());
assert!(auth.open(&[]).is_none());
}
}
}