mod algorithms;
pub mod crypto;
pub use algorithms::{
EncryptionAlgorithm, IntegrityAlgorithm, AUTH_AES_128_GMAC, AUTH_AES_XCBC_96,
AUTH_HMAC_SHA1_96, AUTH_HMAC_SHA2_256_128, AUTH_HMAC_SHA2_384_192, AUTH_HMAC_SHA2_512_256,
AUTH_NONE, ENCR_AES_CBC, ENCR_AES_CCM_8, ENCR_AES_CTR, ENCR_AES_GCM_16, ENCR_CHACHA20_POLY1305,
ENCR_NULL,
};
pub use crypto::{iv_requirement, open, seal, IvRequirement, SealOutput};
use crate::{CrafterError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IpsecMode {
#[default]
Transport,
Tunnel,
}
impl IpsecMode {
pub const fn label(self) -> &'static str {
match self {
Self::Transport => "transport",
Self::Tunnel => "tunnel",
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SecurityAssociation {
pub spi: u32,
pub mode: IpsecMode,
pub enc: EncryptionAlgorithm,
pub enc_key: Vec<u8>,
pub integ: IntegrityAlgorithm,
pub integ_key: Vec<u8>,
pub salt: Vec<u8>,
pub esn: bool,
}
impl SecurityAssociation {
pub const fn new(spi: u32) -> Self {
Self {
spi,
mode: IpsecMode::Transport,
enc: EncryptionAlgorithm::Null,
enc_key: Vec::new(),
integ: IntegrityAlgorithm::None,
integ_key: Vec::new(),
salt: Vec::new(),
esn: false,
}
}
#[must_use]
pub fn encryption(mut self, alg: EncryptionAlgorithm, key: impl Into<Vec<u8>>) -> Self {
self.enc = alg;
self.enc_key = key.into();
self
}
#[must_use]
pub fn integrity(mut self, alg: IntegrityAlgorithm, key: impl Into<Vec<u8>>) -> Self {
self.integ = alg;
self.integ_key = key.into();
self
}
#[must_use]
pub fn salt(mut self, salt: impl Into<Vec<u8>>) -> Self {
self.salt = salt.into();
self
}
#[must_use]
pub fn tunnel(mut self) -> Self {
self.mode = IpsecMode::Tunnel;
self
}
#[must_use]
pub fn transport(mut self) -> Self {
self.mode = IpsecMode::Transport;
self
}
#[must_use]
pub fn extended_sequence(mut self, esn: bool) -> Self {
self.esn = esn;
self
}
pub fn validate(&self) -> Result<()> {
if let Some(expected) = self.enc.key_len() {
if self.enc_key.len() != expected {
return Err(CrafterError::invalid_field_value(
"ipsec.sa.enc_key",
"encryption key length does not match algorithm",
));
}
}
if self.salt.len() != self.enc.salt_len() {
return Err(CrafterError::invalid_field_value(
"ipsec.sa.salt",
"salt length does not match algorithm",
));
}
match self.integ {
IntegrityAlgorithm::None => {}
IntegrityAlgorithm::AesXcbc96 | IntegrityAlgorithm::AesGmac => {
if self.integ_key.len() != 16 {
return Err(CrafterError::invalid_field_value(
"ipsec.sa.integ_key",
"integrity key length does not match algorithm",
));
}
}
_ => {
if self.integ_key.is_empty() {
return Err(CrafterError::invalid_field_value(
"ipsec.sa.integ_key",
"integrity algorithm requires a non-empty key",
));
}
}
}
Ok(())
}
pub fn summary(&self) -> String {
format!(
"SA(spi=0x{:08x}, mode={}, enc={}, integ={}, esn={})",
self.spi,
self.mode.label(),
encryption_label(self.enc),
integrity_label(self.integ),
self.esn,
)
}
}
impl std::fmt::Debug for SecurityAssociation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecurityAssociation")
.field("spi", &format_args!("0x{:08x}", self.spi))
.field("mode", &self.mode)
.field("enc", &self.enc)
.field(
"enc_key",
&format_args!("<{} bytes redacted>", self.enc_key.len()),
)
.field("integ", &self.integ)
.field(
"integ_key",
&format_args!("<{} bytes redacted>", self.integ_key.len()),
)
.field(
"salt",
&format_args!("<{} bytes redacted>", self.salt.len()),
)
.field("esn", &self.esn)
.finish()
}
}
fn encryption_label(alg: EncryptionAlgorithm) -> String {
match alg {
EncryptionAlgorithm::Null => "NULL".to_string(),
EncryptionAlgorithm::AesCbc => "AES_CBC".to_string(),
EncryptionAlgorithm::AesCtr => "AES_CTR".to_string(),
EncryptionAlgorithm::AesCcm8 => "AES_CCM_8".to_string(),
EncryptionAlgorithm::AesGcm16 => "AES_GCM_16".to_string(),
EncryptionAlgorithm::ChaCha20Poly1305 => "CHACHA20_POLY1305".to_string(),
EncryptionAlgorithm::Unknown(id) => format!("UNKNOWN({id})"),
}
}
fn integrity_label(alg: IntegrityAlgorithm) -> String {
match alg {
IntegrityAlgorithm::None => "NONE".to_string(),
IntegrityAlgorithm::HmacSha1_96 => "HMAC_SHA1_96".to_string(),
IntegrityAlgorithm::AesXcbc96 => "AES_XCBC_96".to_string(),
IntegrityAlgorithm::AesGmac => "AES_128_GMAC".to_string(),
IntegrityAlgorithm::HmacSha2_256_128 => "HMAC_SHA2_256_128".to_string(),
IntegrityAlgorithm::HmacSha2_384_192 => "HMAC_SHA2_384_192".to_string(),
IntegrityAlgorithm::HmacSha2_512_256 => "HMAC_SHA2_512_256".to_string(),
IntegrityAlgorithm::Unknown(id) => format!("UNKNOWN({id})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_round_trips_all_fields() {
let sa = SecurityAssociation::new(0x0000_2000)
.encryption(EncryptionAlgorithm::AesGcm16, vec![0xAAu8; 16])
.integrity(IntegrityAlgorithm::None, Vec::new())
.salt(vec![0x01, 0x02, 0x03, 0x04])
.tunnel()
.extended_sequence(true);
assert_eq!(sa.spi, 0x0000_2000);
assert_eq!(sa.mode, IpsecMode::Tunnel);
assert_eq!(sa.enc, EncryptionAlgorithm::AesGcm16);
assert_eq!(sa.enc_key, vec![0xAAu8; 16]);
assert_eq!(sa.integ, IntegrityAlgorithm::None);
assert!(sa.integ_key.is_empty());
assert_eq!(sa.salt, vec![0x01, 0x02, 0x03, 0x04]);
assert!(sa.esn);
let sa = sa.transport().extended_sequence(false);
assert_eq!(sa.mode, IpsecMode::Transport);
assert!(!sa.esn);
}
#[test]
fn new_defaults_are_transport_null_none() {
let sa = SecurityAssociation::new(1);
assert_eq!(sa.spi, 1);
assert_eq!(sa.mode, IpsecMode::Transport);
assert_eq!(sa.enc, EncryptionAlgorithm::Null);
assert_eq!(sa.integ, IntegrityAlgorithm::None);
assert!(sa.enc_key.is_empty());
assert!(sa.integ_key.is_empty());
assert!(sa.salt.is_empty());
assert!(!sa.esn);
}
#[test]
fn validate_accepts_a_correct_aead_sa() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesGcm16, vec![0u8; 16])
.salt(vec![0u8; 4]);
assert!(sa.validate().is_ok());
}
#[test]
fn validate_accepts_a_correct_cbc_hmac_sa() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesCbc, vec![0u8; 16])
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0u8; 32]);
assert!(sa.validate().is_ok());
}
#[test]
fn validate_rejects_wrong_length_encryption_key() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesGcm16, vec![0u8; 8]) .salt(vec![0u8; 4]);
let err = sa.validate().unwrap_err();
assert_eq!(
err,
CrafterError::invalid_field_value(
"ipsec.sa.enc_key",
"encryption key length does not match algorithm",
)
);
assert_eq!(sa.enc_key.len(), 8);
}
#[test]
fn validate_rejects_wrong_length_salt() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesGcm16, vec![0u8; 16])
.salt(vec![0u8; 3]); assert!(sa.validate().is_err());
}
#[test]
fn validate_rejects_empty_hmac_key() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesCbc, vec![0u8; 16])
.integrity(IntegrityAlgorithm::HmacSha2_256_128, Vec::new());
assert!(sa.validate().is_err());
}
#[test]
fn validate_accepts_unknown_algorithm() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::Unknown(99), vec![0u8; 7]);
assert!(sa.validate().is_ok());
}
#[test]
fn summary_omits_key_material() {
let sa = SecurityAssociation::new(0x0000_2000)
.encryption(EncryptionAlgorithm::AesGcm16, vec![0xDEu8; 16])
.salt(vec![0xBEu8; 4]);
let summary = sa.summary();
assert_eq!(
summary,
"SA(spi=0x00002000, mode=transport, enc=AES_GCM_16, integ=NONE, esn=false)"
);
assert!(!summary.contains("dede"));
assert!(!summary.contains("bebe"));
assert!(!summary.to_lowercase().contains("dede"));
assert!(!summary.to_lowercase().contains("bebe"));
}
#[test]
fn debug_redacts_key_material() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesCbc, vec![0xABu8; 16])
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0xCDu8; 32])
.salt(vec![0xEFu8; 0]);
let rendered = format!("{sa:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("171")); assert!(!rendered.contains("ab, ab"));
assert!(!rendered.contains("cd, cd"));
}
}