From e2c0278a4ec0920a751d9fa1eee0f7af088e5b12 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Thu, 19 Feb 2026 11:52:02 +0100 Subject: [PATCH] make the hmac challenge variable length the actual length is still hardcoded as constant, but no longer enforced by the type. --- s2energy-connection/src/pairing/client.rs | 4 ++- s2energy-connection/src/pairing/mod.rs | 6 ++--- s2energy-connection/src/pairing/server.rs | 6 ++++- s2energy-connection/src/pairing/wire.rs | 33 ++++++++++++----------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/s2energy-connection/src/pairing/client.rs b/s2energy-connection/src/pairing/client.rs index 2e5b08e..589c148 100644 --- a/s2energy-connection/src/pairing/client.rs +++ b/s2energy-connection/src/pairing/client.rs @@ -118,7 +118,8 @@ impl<'a> V1Session<'a> { Network::Wan }; - let client_hmac_challenge = HmacChallenge::new(&mut rand::rng()); + const HMAC_CHALLENGE_BYTES: usize = 32; + let client_hmac_challenge = HmacChallenge::new(&mut rand::rng(), HMAC_CHALLENGE_BYTES); let request_pairing_response = self.request_pairing(id, &client_hmac_challenge).await?; let attempt_id = request_pairing_response.pairing_attempt_id; @@ -139,6 +140,7 @@ impl<'a> V1Session<'a> { } } + debug_assert!(request_pairing_response.server_hmac_challenge.0.len() < 32); let server_hmac_challenge_response = match request_pairing_response.selected_hmac_hashing_algorithm { HmacHashingAlgorithm::Sha256 => request_pairing_response.server_hmac_challenge.sha256(&network, pairing_token), }; diff --git a/s2energy-connection/src/pairing/mod.rs b/s2energy-connection/src/pairing/mod.rs index acb7964..2eeeea8 100644 --- a/s2energy-connection/src/pairing/mod.rs +++ b/s2energy-connection/src/pairing/mod.rs @@ -331,8 +331,8 @@ pub struct Pairing { } impl HmacChallenge { - pub fn new(rng: &mut impl Rng) -> Self { - Self(rng.random()) + pub fn new(rng: &mut impl Rng, len: usize) -> Self { + Self(rng.random_iter().take(len).collect()) } pub fn sha256(&self, network: &Network, pairing_token: &[u8]) -> HmacChallengeResponse { @@ -353,7 +353,7 @@ impl HmacChallenge { } } - HmacChallengeResponse(mac.finalize().into_bytes().into()) + HmacChallengeResponse(mac.finalize().into_bytes().to_vec()) } } diff --git a/s2energy-connection/src/pairing/server.rs b/s2energy-connection/src/pairing/server.rs index 9c5f175..e767f76 100644 --- a/s2energy-connection/src/pairing/server.rs +++ b/s2energy-connection/src/pairing/server.rs @@ -253,8 +253,11 @@ async fn v1_request_pairing( return Err(PairingResponseErrorMessage::IncompatibleHMACHashingAlgorithms.into()); } + // 32 bytes is the minimum, this tests that the client can handle more. + const HMAC_CHALLENGE_BYTES: usize = 64; + let mut rng = rand::rng(); - let server_hmac_challenge = HmacChallenge::new(&mut rng); + let server_hmac_challenge = HmacChallenge::new(&mut rng, HMAC_CHALLENGE_BYTES); let open_pairing = { let mut open_pairings = state.open_pairings.lock().unwrap(); @@ -297,6 +300,7 @@ async fn v1_request_pairing( } } + debug_assert!(request_pairing.client_hmac_challenge.0.len() < 32); let client_hmac_challenge_response = request_pairing.client_hmac_challenge.sha256(&state.network, &open_pairing.token.0); let pairing_attempt_id = { diff --git a/s2energy-connection/src/pairing/wire.rs b/s2energy-connection/src/pairing/wire.rs index 2751496..1f33e55 100644 --- a/s2energy-connection/src/pairing/wire.rs +++ b/s2energy-connection/src/pairing/wire.rs @@ -34,20 +34,26 @@ pub(crate) enum HmacHashingAlgorithm { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub(crate) struct HmacChallenge( - #[serde( - serialize_with = "base64_bytes::serialize", - deserialize_with = "base64_bytes::deserialize::<_, 32>" - )] - pub(crate) [u8; 32], + #[serde(serialize_with = "base64_bytes::serialize", deserialize_with = "deserialize_hmac_challenge")] pub(crate) Vec, ); +pub(crate) fn deserialize_hmac_challenge<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let decoded = base64_bytes::deserialize(deserializer)?; + + // The spec demands that an hmac challenge is at least 32 bytes. + if decoded.len() < 32 { + return Err(de::Error::custom("hmac challenge shorter than 32 bytes")); + } + + Ok(decoded) +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub(crate) struct HmacChallengeResponse( - #[serde( - serialize_with = "base64_bytes::serialize", - deserialize_with = "base64_bytes::deserialize::<_, 32>" - )] - pub(crate) [u8; 32], + #[serde(serialize_with = "base64_bytes::serialize", deserialize_with = "base64_bytes::deserialize")] pub(crate) Vec, ); #[derive(Serialize, Deserialize)] @@ -150,16 +156,13 @@ mod base64_bytes { serializer.serialize_str(&encoded) } - pub(crate) fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error> + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; let decoded = STANDARD.decode(&s).map_err(de::Error::custom)?; - decoded - .as_slice() - .try_into() - .map_err(|_| de::Error::custom(format!("expected {N} bytes after base64 decoding, got {}", decoded.len()))) + Ok(decoded) } }