use crate::error::{Error as PkeError, Result as PkeResult};
use dcrypt_algorithms::hash::sha2::{Sha256, Sha384, Sha512}; use dcrypt_algorithms::kdf::hkdf::Hkdf;
use dcrypt_algorithms::kdf::KeyDerivationFunction; use dcrypt_api::error::Error as ApiError;
use dcrypt_internal::zeroing::{boxed_bytes_zeroed, Zeroizing, ZeroizingBytes};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub mod p224;
pub mod p256;
pub mod p384;
pub mod p521;
pub use p224::EciesP224;
pub use p256::EciesP256;
pub use p384::EciesP384;
pub use p521::EciesP521;
pub(crate) const CHACHA20POLY1305_KEY_LEN: usize = 32;
pub(crate) const AES256GCM_KEY_LEN: usize = 32;
pub(crate) const CHACHA20POLY1305_NONCE_LEN: usize = 12;
pub(crate) const AES256GCM_NONCE_LEN: usize = 12;
pub(crate) const AEAD_TAG_LEN: usize = 16;
const KDF_SALT: &[u8] = b"dcrypt-v3/ECIES/extract";
pub(crate) fn invalid_recipient_public_key() -> ApiError {
ApiError::InvalidKey {
context: "ECIES recipient public key",
#[cfg(feature = "std")]
message: "recipient public key is not a canonical non-identity curve point".to_string(),
}
}
pub(crate) fn invalid_ephemeral_public_key() -> ApiError {
ApiError::InvalidCiphertext {
context: "ECIES ephemeral public key",
#[cfg(feature = "std")]
message: "ephemeral public key is not a canonical non-identity curve point".to_string(),
}
}
fn kdf_transcript(
shared_secret_z: &[u8],
ephemeral_pk_bytes: &[u8],
recipient_pk_bytes: &[u8],
) -> PkeResult<ZeroizingBytes> {
let total_len = shared_secret_z
.len()
.checked_add(ephemeral_pk_bytes.len())
.and_then(|len| len.checked_add(recipient_pk_bytes.len()))
.ok_or(PkeError::SerializationError(
"ECIES KDF transcript length overflows the platform address space",
))?;
let mut transcript = Zeroizing::new(boxed_bytes_zeroed(total_len));
let ephemeral_start = shared_secret_z.len();
let recipient_start = ephemeral_start + ephemeral_pk_bytes.len();
transcript[..ephemeral_start].copy_from_slice(shared_secret_z);
transcript[ephemeral_start..recipient_start].copy_from_slice(ephemeral_pk_bytes);
transcript[recipient_start..].copy_from_slice(recipient_pk_bytes);
Ok(transcript)
}
pub(crate) fn derive_symmetric_key_hkdf_sha256(
shared_secret_z: &[u8],
ephemeral_pk_bytes: &[u8],
recipient_pk_bytes: &[u8],
key_output_len: usize,
info: &[u8],
) -> PkeResult<ZeroizingBytes> {
let transcript = kdf_transcript(shared_secret_z, ephemeral_pk_bytes, recipient_pk_bytes)?;
let kdf = Hkdf::<Sha256>::new();
let key = kdf
.derive_key(&transcript, Some(KDF_SALT), Some(info), key_output_len)
.map_err(PkeError::from)?;
Ok(key)
}
pub(crate) fn derive_symmetric_key_hkdf_sha384(
shared_secret_z: &[u8],
ephemeral_pk_bytes: &[u8],
recipient_pk_bytes: &[u8],
key_output_len: usize,
info: &[u8],
) -> PkeResult<ZeroizingBytes> {
let transcript = kdf_transcript(shared_secret_z, ephemeral_pk_bytes, recipient_pk_bytes)?;
let kdf = Hkdf::<Sha384>::new();
let key = kdf
.derive_key(&transcript, Some(KDF_SALT), Some(info), key_output_len)
.map_err(PkeError::from)?;
Ok(key)
}
pub(crate) fn derive_symmetric_key_hkdf_sha512(
shared_secret_z: &[u8],
ephemeral_pk_bytes: &[u8],
recipient_pk_bytes: &[u8],
key_output_len: usize,
info: &[u8],
) -> PkeResult<ZeroizingBytes> {
let transcript = kdf_transcript(shared_secret_z, ephemeral_pk_bytes, recipient_pk_bytes)?;
let kdf = Hkdf::<Sha512>::new();
let key = kdf
.derive_key(&transcript, Some(KDF_SALT), Some(info), key_output_len)
.map_err(PkeError::from)?;
Ok(key)
}
#[derive(Clone, Debug)]
pub(crate) struct EciesCiphertextComponents {
pub ephemeral_public_key: Vec<u8>, pub aead_nonce: Vec<u8>, pub aead_ciphertext_tag: Vec<u8>, }
impl EciesCiphertextComponents {
pub fn serialize(&self) -> PkeResult<Vec<u8>> {
let r_len = self.ephemeral_public_key.len();
let n_len = self.aead_nonce.len();
let ct_t_len = self.aead_ciphertext_tag.len();
if r_len > u8::MAX as usize {
return Err(PkeError::SerializationError(
"ephemeral public key exceeds the wire-format limit",
));
}
if n_len > u8::MAX as usize {
return Err(PkeError::SerializationError(
"AEAD nonce exceeds the wire-format limit",
));
}
if ct_t_len > u32::MAX as usize {
return Err(PkeError::SerializationError(
"AEAD payload exceeds the wire-format limit",
));
}
let total_len = 1usize
.checked_add(r_len)
.and_then(|len| len.checked_add(1))
.and_then(|len| len.checked_add(n_len))
.and_then(|len| len.checked_add(4))
.and_then(|len| len.checked_add(ct_t_len))
.ok_or(PkeError::SerializationError(
"ECIES ciphertext length overflows the platform address space",
))?;
let mut serialized = Vec::new();
serialized
.try_reserve_exact(total_len)
.map_err(|_| PkeError::SerializationError("unable to allocate ECIES ciphertext"))?;
serialized.push(r_len as u8);
serialized.extend_from_slice(&self.ephemeral_public_key);
serialized.push(n_len as u8);
serialized.extend_from_slice(&self.aead_nonce);
serialized.extend_from_slice(&(ct_t_len as u32).to_be_bytes());
serialized.extend_from_slice(&self.aead_ciphertext_tag);
Ok(serialized)
}
pub fn deserialize(bytes: &[u8]) -> PkeResult<Self> {
if bytes.is_empty() {
return Err(PkeError::InvalidCiphertextFormat(
"empty input for deserialization",
));
}
let mut current_pos = 0;
if bytes.len() < current_pos + 1 {
return Err(PkeError::InvalidCiphertextFormat("R length truncated"));
}
let r_len = bytes[current_pos] as usize;
current_pos += 1;
let r_end = current_pos
.checked_add(r_len)
.ok_or(PkeError::InvalidCiphertextFormat(
"R length overflows the platform address space",
))?;
if bytes.len() < r_end {
return Err(PkeError::InvalidCiphertextFormat("R data truncated"));
}
let r_start = current_pos;
current_pos = r_end;
if bytes.len() < current_pos + 1 {
return Err(PkeError::InvalidCiphertextFormat("Nonce length truncated"));
}
let n_len = bytes[current_pos] as usize;
current_pos += 1;
let nonce_end = current_pos
.checked_add(n_len)
.ok_or(PkeError::InvalidCiphertextFormat(
"nonce length overflows the platform address space",
))?;
if bytes.len() < nonce_end {
return Err(PkeError::InvalidCiphertextFormat("Nonce data truncated"));
}
let nonce_start = current_pos;
current_pos = nonce_end;
if bytes.len() < current_pos + 4 {
return Err(PkeError::InvalidCiphertextFormat(
"AEAD payload length truncated",
));
}
let ct_t_len = u32::from_be_bytes(
bytes[current_pos..current_pos + 4]
.try_into()
.map_err(|_| {
PkeError::InvalidCiphertextFormat("Failed to read AEAD payload length")
})?,
) as usize;
current_pos += 4;
let payload_end =
current_pos
.checked_add(ct_t_len)
.ok_or(PkeError::InvalidCiphertextFormat(
"AEAD payload length overflows the platform address space",
))?;
if bytes.len() < payload_end {
return Err(PkeError::InvalidCiphertextFormat(
"AEAD payload data truncated",
));
}
if payload_end != bytes.len() {
return Err(PkeError::InvalidCiphertextFormat(
"trailing data after deserialization",
));
}
let ephemeral_public_key = bytes[r_start..r_end].to_vec();
let aead_nonce = bytes[nonce_start..nonce_end].to_vec();
let aead_ciphertext_tag = bytes[current_pos..payload_end].to_vec();
Ok(Self {
ephemeral_public_key,
aead_nonce,
aead_ciphertext_tag,
})
}
}
#[cfg(test)]
mod framing_tests {
use super::EciesCiphertextComponents;
#[test]
fn framing_roundtrip_is_exact() {
let components = EciesCiphertextComponents {
ephemeral_public_key: vec![4, 1, 2, 3],
aead_nonce: vec![5; 12],
aead_ciphertext_tag: vec![6; 31],
};
let encoded = components.serialize().unwrap();
let decoded = EciesCiphertextComponents::deserialize(&encoded).unwrap();
assert_eq!(
decoded.ephemeral_public_key,
components.ephemeral_public_key
);
assert_eq!(decoded.aead_nonce, components.aead_nonce);
assert_eq!(decoded.aead_ciphertext_tag, components.aead_ciphertext_tag);
}
#[test]
fn serializer_rejects_unrepresentable_component_lengths() {
let oversized_key = EciesCiphertextComponents {
ephemeral_public_key: vec![0; u8::MAX as usize + 1],
aead_nonce: Vec::new(),
aead_ciphertext_tag: Vec::new(),
};
assert!(oversized_key.serialize().is_err());
let oversized_nonce = EciesCiphertextComponents {
ephemeral_public_key: Vec::new(),
aead_nonce: vec![0; u8::MAX as usize + 1],
aead_ciphertext_tag: Vec::new(),
};
assert!(oversized_nonce.serialize().is_err());
}
#[test]
fn decoder_rejects_truncation_and_trailing_data() {
let truncated_payload = [0, 0, 0, 0, 0, 1];
assert!(EciesCiphertextComponents::deserialize(&truncated_payload).is_err());
let trailing_data = [0, 0, 0, 0, 0, 0, 1];
assert!(EciesCiphertextComponents::deserialize(&trailing_data).is_err());
let maximum_declared_payload = [0, 0, 0xff, 0xff, 0xff, 0xff];
assert!(EciesCiphertextComponents::deserialize(&maximum_declared_payload).is_err());
let mut maximum_key_then_truncated = vec![u8::MAX];
maximum_key_then_truncated.extend_from_slice(&[0; u8::MAX as usize]);
assert!(EciesCiphertextComponents::deserialize(&maximum_key_then_truncated).is_err());
}
}