use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::{Error, Result};
pub(crate) const PREFIX: &[u8] = b"v1";
pub(crate) const MAC_LEN: usize = 32;
type HmacSha256 = Hmac<Sha256>;
fn mac_key(key: &[u8]) -> [u8; MAC_LEN] {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key size");
mac.update(b"dgn:enc:hmac");
mac.finalize().into_bytes().into()
}
pub(crate) fn pack(iv: &[u8], ciphertext: &[u8], key: &[u8]) -> Vec<u8> {
let mac = {
let mut mac = HmacSha256::new_from_slice(&mac_key(key)).expect("HMAC accepts any key size");
mac.update(iv);
mac.update(ciphertext);
mac.finalize().into_bytes()
};
let mut blob = Vec::with_capacity(PREFIX.len() + iv.len() + MAC_LEN + ciphertext.len());
blob.extend_from_slice(PREFIX);
blob.extend_from_slice(iv);
blob.extend_from_slice(&mac);
blob.extend_from_slice(ciphertext);
blob
}
pub(crate) fn unpack_and_verify<'a>(
blob: &'a [u8],
iv_len: usize,
key: &[u8],
label: &'static str,
) -> Result<(&'a [u8], &'a [u8])> {
let body = blob
.strip_prefix(PREFIX)
.ok_or(Error::InvalidPrefix { label })?;
if body.len() < iv_len + MAC_LEN {
return Err(Error::TooShort { label });
}
let (iv, rest) = body.split_at(iv_len);
let (mac, ciphertext) = rest.split_at(MAC_LEN);
let mut verifier =
HmacSha256::new_from_slice(&mac_key(key)).expect("HMAC accepts any key size");
verifier.update(iv);
verifier.update(ciphertext);
verifier
.verify_slice(mac)
.map_err(|_| Error::MacVerificationFailed { label })?;
Ok((iv, ciphertext))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_then_verify_roundtrip() {
let key = [1u8; 16];
let blob = pack(&[2u8; 16], &[3u8; 8], &key);
let (iv, ct) = unpack_and_verify(&blob, 16, &key, "TEST").unwrap();
assert_eq!(iv, &[2u8; 16]);
assert_eq!(ct, &[3u8; 8]);
}
#[test]
fn tampered_blob_fails_mac() {
let key = [1u8; 16];
let mut blob = pack(&[2u8; 16], &[3u8; 8], &key);
let last = blob.len() - 1;
blob[last] ^= 0x01;
assert_eq!(
unpack_and_verify(&blob, 16, &key, "TEST").unwrap_err(),
Error::MacVerificationFailed { label: "TEST" }
);
}
#[test]
fn wrong_prefix_and_short_blob() {
let key = [1u8; 16];
assert_eq!(
unpack_and_verify(b"v9short", 16, &key, "TEST").unwrap_err(),
Error::InvalidPrefix { label: "TEST" }
);
assert_eq!(
unpack_and_verify(b"v1tooshort", 16, &key, "TEST").unwrap_err(),
Error::TooShort { label: "TEST" }
);
}
}