use rand_core::{CryptoRng, CryptoRngCore, OsRng, RngCore};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::constants::{AES_GCM_NONCE_SIZE, AES_GCM_TAG_SIZE, EC_PUBKEY_COMPRESSED_SIZE, TIGHTBEAM_ECIES_KDF_INFO};
use crate::crypto::aead::{Aead, AeadCore, Key, KeyInit, Nonce, Payload};
use crate::crypto::common::{typenum::Unsigned, KeySizeUser};
use crate::crypto::k256::ecdh::{diffie_hellman, EphemeralSecret};
use crate::crypto::k256::elliptic_curve::sec1::ToEncodedPoint;
use crate::crypto::k256::{PublicKey, SecretKey};
use crate::crypto::kdf::{ecies_kdf, KdfError, KdfFunction};
use crate::crypto::secret::{Secret, SecretSlice};
use crate::random::RngWrapper;
#[cfg(feature = "x509")]
use crate::asn1::ObjectIdentifier;
#[cfg(feature = "x509")]
use crate::crypto::aead::Aes256Gcm;
#[cfg(feature = "x509")]
use crate::crypto::kdf::HkdfSha3_256;
#[cfg(feature = "x509")]
use crate::der::oid::AssociatedOid;
pub trait EciesPublicKeyOps: Clone + PartialEq + Eq {
type SecretKey: EciesSecretKeyOps<PublicKey = Self>;
const PUBLIC_KEY_SIZE: usize;
fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self>
where
Self: Sized;
fn to_bytes(&self) -> Vec<u8>;
}
pub trait EciesSecretKeyOps: Clone {
type PublicKey: EciesPublicKeyOps;
const SECRET_KEY_SIZE: usize;
fn random<R: CryptoRng + RngCore>(rng: &mut R) -> Self;
fn public_key(&self) -> Self::PublicKey;
fn diffie_hellman(&self, public_key: &Self::PublicKey) -> SecretSlice<u8>;
}
pub trait EciesEphemeral {
type PublicKey: EciesPublicKeyOps;
fn generate_ephemeral(
recipient_pubkey: &Self::PublicKey,
rng: &mut dyn rand_core::CryptoRngCore,
) -> Result<(Vec<u8>, SecretSlice<u8>)>;
}
#[derive(Debug, Clone)]
pub enum EciesError {
InvalidCiphertext,
InvalidPublicKey(crate::crypto::k256::elliptic_curve::Error),
InvalidSecretKey(crate::crypto::k256::elliptic_curve::Error),
EncryptionFailed(crate::crypto::aead::Error),
DecryptionFailed(crate::crypto::aead::Error),
Kdf(KdfError),
SecretUnavailable(crate::crypto::secret::SecretError),
}
crate::impl_error_display!(unconditional EciesError {
InvalidCiphertext => "Invalid ECIES ciphertext format",
InvalidPublicKey(e) => "Invalid ECIES public key: {e}",
InvalidSecretKey(e) => "Invalid ECIES secret key: {e}",
EncryptionFailed(e) => "ECIES encryption failed: {e}",
DecryptionFailed(e) => "ECIES decryption failed: {e}",
Kdf(e) => "ECIES key derivation failed: {e}",
SecretUnavailable(e) => "Secret unavailable: {e}",
});
crate::impl_from!(KdfError => EciesError::Kdf);
crate::impl_from!(crate::crypto::secret::SecretError => EciesError::SecretUnavailable);
pub type Result<T> = core::result::Result<T, EciesError>;
impl EciesPublicKeyOps for PublicKey {
type SecretKey = SecretKey;
const PUBLIC_KEY_SIZE: usize = EC_PUBKEY_COMPRESSED_SIZE;
fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
PublicKey::from_sec1_bytes(bytes.as_ref()).map_err(EciesError::InvalidPublicKey)
}
fn to_bytes(&self) -> Vec<u8> {
let point = self.to_encoded_point(true);
point.as_bytes().to_vec()
}
}
impl EciesSecretKeyOps for SecretKey {
type PublicKey = PublicKey;
const SECRET_KEY_SIZE: usize = 32;
fn random<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
SecretKey::random(rng)
}
fn public_key(&self) -> Self::PublicKey {
SecretKey::public_key(self)
}
fn diffie_hellman(&self, public_key: &Self::PublicKey) -> SecretSlice<u8> {
let shared_secret = diffie_hellman(self.to_nonzero_scalar(), public_key.as_affine());
let v = shared_secret.raw_secret_bytes().to_vec().into_boxed_slice();
Secret::from(v)
}
}
impl core::convert::TryFrom<SecretSlice<u8>> for SecretKey {
type Error = EciesError;
fn try_from(bytes: SecretSlice<u8>) -> Result<Self> {
use crate::crypto::secret::ToInsecure;
let raw = bytes.to_insecure().map_err(EciesError::from)?;
SecretKey::from_slice(&raw).map_err(EciesError::InvalidSecretKey)
}
}
impl From<&SecretKey> for SecretSlice<u8> {
fn from(sk: &SecretKey) -> Self {
let v = SecretKey::to_bytes(sk).to_vec();
Secret::from(v.into_boxed_slice())
}
}
impl EciesEphemeral for SecretKey {
type PublicKey = PublicKey;
fn generate_ephemeral(
recipient_pubkey: &Self::PublicKey,
rng: &mut dyn CryptoRngCore,
) -> Result<(Vec<u8>, SecretSlice<u8>)> {
let mut wrapper = RngWrapper(rng);
let ephemeral_secret = EphemeralSecret::random(&mut wrapper);
let ephemeral_pubkey = ephemeral_secret.public_key();
let shared_secret = ephemeral_secret.diffie_hellman(recipient_pubkey);
let ephemeral_point = ephemeral_pubkey.to_encoded_point(true);
let ephemeral_bytes = ephemeral_point.as_bytes().to_vec();
let shared_bytes = Secret::from(shared_secret.raw_secret_bytes().to_vec().into_boxed_slice());
Ok((ephemeral_bytes, shared_bytes))
}
}
pub trait EciesMessageOps: Sized {
const PUBKEY_SIZE: usize;
fn from_bytes(bytes: &[u8]) -> Result<Self>;
fn to_bytes(&self) -> Vec<u8>;
fn ephemeral_pubkey(&self) -> &[u8];
fn ciphertext(&self) -> &[u8];
}
pub struct Secp256k1EciesMessage {
ephemeral_pubkey: Vec<u8>,
ciphertext: Vec<u8>,
}
impl Secp256k1EciesMessage {
const MIN_CIPHERTEXT_SIZE: usize = AES_GCM_NONCE_SIZE + AES_GCM_TAG_SIZE;
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
let bytes = bytes.as_ref();
if bytes.len() < EC_PUBKEY_COMPRESSED_SIZE + Self::MIN_CIPHERTEXT_SIZE {
return Err(EciesError::InvalidCiphertext);
}
let ephemeral_pubkey = bytes[0..EC_PUBKEY_COMPRESSED_SIZE].to_vec();
let ciphertext = bytes[EC_PUBKEY_COMPRESSED_SIZE..].to_vec();
if ciphertext.len() < Self::MIN_CIPHERTEXT_SIZE {
return Err(EciesError::InvalidCiphertext);
}
Ok(Self { ephemeral_pubkey, ciphertext })
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.ephemeral_pubkey.len() + self.ciphertext.len());
bytes.extend_from_slice(&self.ephemeral_pubkey);
bytes.extend_from_slice(&self.ciphertext);
bytes
}
#[cfg(test)]
pub(crate) fn ephemeral_pubkey_mut(&mut self) -> &mut Vec<u8> {
&mut self.ephemeral_pubkey
}
#[cfg(test)]
pub(crate) fn ciphertext_mut(&mut self) -> &mut Vec<u8> {
&mut self.ciphertext
}
}
impl EciesMessageOps for Secp256k1EciesMessage {
const PUBKEY_SIZE: usize = EC_PUBKEY_COMPRESSED_SIZE;
fn from_bytes(bytes: &[u8]) -> Result<Self> {
Self::from_bytes(bytes)
}
fn to_bytes(&self) -> Vec<u8> {
Self::to_bytes(self)
}
fn ephemeral_pubkey(&self) -> &[u8] {
&self.ephemeral_pubkey
}
fn ciphertext(&self) -> &[u8] {
&self.ciphertext
}
}
pub fn encrypt<PK, P, R, M, K, A>(
recipient_pubkey: &PK,
plaintext: P,
associated_data: Option<&[u8]>,
rng: Option<&mut R>,
) -> Result<M>
where
PK: EciesPublicKeyOps,
PK::SecretKey: EciesEphemeral<PublicKey = PK>,
P: AsRef<[u8]>,
R: CryptoRng + RngCore,
M: EciesMessageOps,
K: KdfFunction,
A: Aead + KeyInit,
{
let plaintext = plaintext.as_ref();
let key_size = <A as KeySizeUser>::KeySize::USIZE;
macro_rules! do_encrypt {
($rng:expr) => {{
let (ephemeral_bytes, shared_secret) = PK::SecretKey::generate_ephemeral(recipient_pubkey, $rng)?;
let k_enc = ecies_kdf::<K>(&ephemeral_bytes, shared_secret, TIGHTBEAM_ECIES_KDF_INFO, None)?;
let key = Key::<A>::from_slice(&k_enc[..key_size]);
let cipher = A::new(key);
let mut nonce = Nonce::<A>::default();
$rng.fill_bytes(nonce.as_mut_slice());
let payload = match associated_data {
Some(aad) => Payload { msg: plaintext, aad },
None => Payload { msg: plaintext, aad: b"" },
};
let ciphertext = cipher.encrypt(&nonce, payload).map_err(EciesError::EncryptionFailed)?;
let encrypted_len = ciphertext.len();
let mut final_ciphertext = Vec::with_capacity(nonce.len() + encrypted_len);
final_ciphertext.extend_from_slice(nonce.as_slice());
final_ciphertext.extend_from_slice(&ciphertext);
let total_len = ephemeral_bytes.len() + final_ciphertext.len();
let mut wire_bytes = Vec::with_capacity(total_len);
wire_bytes.extend_from_slice(&ephemeral_bytes);
wire_bytes.extend_from_slice(&final_ciphertext);
M::from_bytes(&wire_bytes)
}};
}
match rng {
Some(r) => do_encrypt!(r),
None => do_encrypt!(&mut OsRng),
}
}
pub fn decrypt<SK, M, K, A>(
recipient_seckey: &SK,
message: &M,
associated_data: Option<&[u8]>,
) -> Result<SecretSlice<u8>>
where
SK: EciesSecretKeyOps,
M: EciesMessageOps,
K: KdfFunction,
A: Aead + KeyInit,
{
let ephemeral_pubkey = <SK::PublicKey as EciesPublicKeyOps>::from_bytes(message.ephemeral_pubkey())?;
let shared_secret = recipient_seckey.diffie_hellman(&ephemeral_pubkey);
decrypt_with_shared_secret::<M, K, A>(message, shared_secret, associated_data)
}
pub fn decrypt_with_shared_secret<M, K, A>(
message: &M,
shared_secret: SecretSlice<u8>,
associated_data: Option<&[u8]>,
) -> Result<SecretSlice<u8>>
where
M: EciesMessageOps,
K: KdfFunction,
A: Aead + KeyInit,
{
let k_enc = ecies_kdf::<K>(message.ephemeral_pubkey(), shared_secret, TIGHTBEAM_ECIES_KDF_INFO, None)?;
let nonce_size = <A as AeadCore>::NonceSize::USIZE;
let tag_size = <A as AeadCore>::TagSize::USIZE;
let key_size = <A as KeySizeUser>::KeySize::USIZE;
let ciphertext_bytes = message.ciphertext();
if ciphertext_bytes.len() < nonce_size + tag_size {
return Err(EciesError::InvalidCiphertext);
}
let nonce = Nonce::<A>::from_slice(&ciphertext_bytes[..nonce_size]);
let ciphertext_with_tag = &ciphertext_bytes[nonce_size..];
let key = Key::<A>::from_slice(&k_enc[..key_size]);
let cipher = A::new(key);
let payload = match associated_data {
Some(aad) => Payload { msg: ciphertext_with_tag, aad },
None => Payload { msg: ciphertext_with_tag, aad: b"" },
};
let plaintext = cipher.decrypt(nonce, payload).map_err(EciesError::DecryptionFailed)?;
Ok(Secret::from(plaintext.into_boxed_slice()))
}
pub fn ephemeral_pubkey_bytes<M>(bytes: &[u8]) -> Result<&[u8]>
where
M: EciesMessageOps,
{
bytes.get(..M::PUBKEY_SIZE).ok_or(EciesError::InvalidCiphertext)
}
#[cfg(feature = "x509")]
crate::define_oid_wrapper!(
EciesSecp256k1Oid,
"1.3.132.1.12.0"
);
#[cfg(feature = "x509")]
pub struct EciesEncryptor {
recipient_pubkey: PublicKey,
}
#[cfg(feature = "x509")]
impl EciesEncryptor {
pub fn new(recipient_pubkey: PublicKey) -> Self {
Self { recipient_pubkey }
}
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
let pubkey = PublicKey::from_bytes(bytes)?;
Ok(Self::new(pubkey))
}
}
#[cfg(feature = "x509")]
impl crate::crypto::aead::Encryptor<EciesSecp256k1Oid> for EciesEncryptor {
fn encrypt_content(
&self,
data: impl AsRef<[u8]>,
_nonce: impl AsRef<[u8]>, content_type: Option<ObjectIdentifier>,
) -> crate::error::Result<crate::EncryptedContentInfo> {
let ecies_msg = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&self.recipient_pubkey,
data.as_ref(),
None,
None::<&mut OsRng>,
)?;
let encrypted_bytes = ecies_msg.to_bytes();
let content_type = content_type.unwrap_or(crate::oids::DATA);
let content_enc_alg = crate::AlgorithmIdentifier { oid: EciesSecp256k1Oid::OID, parameters: None };
let encrypted_content = Some(crate::der::asn1::OctetString::new(encrypted_bytes)?);
Ok(crate::EncryptedContentInfo { content_type, content_enc_alg, encrypted_content })
}
}
#[cfg(feature = "x509")]
pub struct EciesDecryptor {
secret_key: SecretKey,
}
#[cfg(feature = "x509")]
impl EciesDecryptor {
pub fn new(secret_key: SecretKey) -> Self {
Self { secret_key }
}
}
#[cfg(feature = "x509")]
impl crate::crypto::aead::Decryptor for EciesDecryptor {
fn decrypt_content(&self, info: &crate::EncryptedContentInfo) -> crate::error::Result<SecretSlice<u8>> {
let encrypted_bytes = info
.encrypted_content
.as_ref()
.ok_or(crate::TightBeamError::MissingEncryptionInfo)?
.as_bytes();
let ecies_msg = Secp256k1EciesMessage::from_bytes(encrypted_bytes)?;
Ok(decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&self.secret_key, &ecies_msg, None)?)
}
}
#[cfg(all(
feature = "digest",
feature = "aead",
feature = "signature",
feature = "kdf",
feature = "ecdh"
))]
pub struct EciesSharedSecretDecryptor<P> {
shared_secret: SecretSlice<u8>,
_provider: core::marker::PhantomData<P>,
}
#[cfg(all(
feature = "digest",
feature = "aead",
feature = "signature",
feature = "kdf",
feature = "ecdh"
))]
impl<P> EciesSharedSecretDecryptor<P> {
pub fn new(shared_secret: impl Into<SecretSlice<u8>>) -> Self {
Self { shared_secret: shared_secret.into(), _provider: core::marker::PhantomData }
}
}
#[cfg(all(
feature = "digest",
feature = "aead",
feature = "signature",
feature = "kdf",
feature = "ecdh"
))]
impl<P> crate::crypto::aead::Decryptor for EciesSharedSecretDecryptor<P>
where
P: crate::crypto::profiles::CryptoProvider,
P::AeadCipher: KeyInit,
{
fn decrypt_content(&self, info: &crate::EncryptedContentInfo) -> crate::error::Result<SecretSlice<u8>> {
let encrypted_bytes = info
.encrypted_content
.as_ref()
.ok_or(crate::TightBeamError::MissingEncryptionInfo)?
.as_bytes();
let ecies_msg = <P::EciesMessage as EciesMessageOps>::from_bytes(encrypted_bytes)?;
let shared_secret = self.shared_secret.with(|bytes| SecretSlice::from(bytes.to_vec()))?;
Ok(decrypt_with_shared_secret::<P::EciesMessage, P::Kdf, P::AeadCipher>(
&ecies_msg,
shared_secret,
None,
)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::secret::ToInsecure;
use rand_core::OsRng;
fn keypair() -> (SecretKey, PublicKey) {
let mut rng = OsRng;
let secret = SecretKey::random(&mut rng);
let public = secret.public_key();
(secret, public)
}
fn roundtrip(plaintext: &[u8], aad: Option<&[u8]>) -> Result<()> {
let (secret, public) = keypair();
let encrypted = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&public,
plaintext,
aad,
None::<&mut OsRng>,
)?;
let decrypted = decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&secret, &encrypted, aad)?;
assert_eq!(plaintext, &decrypted.to_insecure().map_err(EciesError::from)?[..]);
Ok(())
}
#[test]
fn test_ecies_encryption() -> Result<()> {
let cases = [
(&b"Hello, ECIES!"[..], None),
(b"Secret message", Some(&b"authenticated data"[..])),
(b"", None),
(b"The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", None),
(b"\x00\x01\x02\x03\xFF\xFE\xFD\xFC", None),
(b"Payload", Some(&b"version:1|timestamp:12345|nonce:abcdef"[..])),
];
for (plaintext, aad) in cases {
roundtrip(plaintext, aad)?;
}
Ok(())
}
#[test]
fn test_aad_validation() -> Result<()> {
let mut rng = OsRng;
let (secret, public) = keypair();
let plaintext = b"Secret message";
let correct_aad = b"authenticated data";
let encrypted = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&public,
plaintext,
Some(correct_aad),
Some(&mut rng),
)?;
let cases = [
(Some(&correct_aad[..]), true),
(Some(&b"wrong data"[..]), false),
(None, false),
(Some(&b""[..]), false),
];
for (aad, should_succeed) in cases {
let result = decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&secret, &encrypted, aad);
assert_eq!(result.is_ok(), should_succeed);
if should_succeed {
assert_eq!(&plaintext[..], &result?.to_insecure().map_err(EciesError::from)?[..]);
}
}
Ok(())
}
#[test]
fn test_serialization() -> Result<()> {
let (secret, public) = keypair();
let plaintext = b"Test serialization";
let encrypted = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&public,
plaintext,
None,
None::<&mut OsRng>,
)?;
let bytes = encrypted.to_bytes();
let parsed = Secp256k1EciesMessage::from_bytes(&bytes)?;
let decrypted = decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&secret, &parsed, None)?;
assert_eq!(&plaintext[..], &decrypted.to_insecure().map_err(EciesError::from)?[..]);
let secret_bytes: SecretSlice<u8> = (&secret).into();
let public_bytes = public.to_bytes();
let secret2 = SecretKey::try_from(secret_bytes)?;
let public2 = PublicKey::from_bytes(&public_bytes)?;
assert_eq!(public.to_bytes(), public2.to_bytes());
assert_eq!(secret.public_key().to_bytes(), secret2.public_key().to_bytes());
Ok(())
}
#[test]
fn test_security_properties() -> Result<()> {
let plaintext = b"Sensitive data";
let (_, public1) = keypair();
let (secret2, _) = keypair();
let encrypted = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&public1,
plaintext,
None,
None::<&mut OsRng>,
)?;
assert!(decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&secret2, &encrypted, None).is_err());
let (secret, public) = keypair();
let tamper_functions: [fn(&mut Secp256k1EciesMessage); 4] = [
|msg| {
if let Some(byte) = msg.ciphertext_mut().last_mut() {
*byte ^= 0xFF;
}
},
|msg| {
if let Some(byte) = msg.ciphertext_mut().first_mut() {
*byte ^= 0xFF;
}
},
|msg| {
let len = msg.ciphertext_mut().len().saturating_sub(1);
msg.ciphertext_mut().truncate(len);
},
|msg| {
if let Some(byte) = msg.ephemeral_pubkey_mut().first_mut() {
*byte ^= 0xFF;
}
},
];
for tamper_fn in tamper_functions {
let mut encrypted = encrypt::<_, _, _, Secp256k1EciesMessage, HkdfSha3_256, Aes256Gcm>(
&public,
plaintext,
None,
None::<&mut OsRng>,
)?;
tamper_fn(&mut encrypted);
assert!(decrypt::<_, _, HkdfSha3_256, Aes256Gcm>(&secret, &encrypted, None).is_err());
}
Ok(())
}
#[test]
fn test_edge_cases() -> Result<()> {
let invalid_ciphertexts = [
vec![],
vec![0u8; 32],
vec![0u8; 33], vec![0u8; 45], ];
for data in invalid_ciphertexts {
assert!(Secp256k1EciesMessage::from_bytes(&data).is_err());
}
assert!(PublicKey::from_bytes([0xFFu8; 33]).is_err());
assert!(SecretKey::try_from(Secret::from(vec![0x00u8; 32].into_boxed_slice())).is_err());
Ok(())
}
#[cfg(all(feature = "x509", feature = "signature", feature = "ecdh", feature = "tokio"))]
#[tokio::test]
async fn shared_secret_decryptor_via_provider() -> crate::error::Result<()> {
use crate::crypto::aead::{Decryptor, Encryptor};
use crate::crypto::key::{Secp256k1KeyProvider, SigningKeyProvider};
use crate::crypto::profiles::DefaultCryptoProvider;
use crate::crypto::sign::ecdsa::Secp256k1SigningKey;
let plaintext = b"hsm-backed ecies decryption";
let (secret, public) = keypair();
let info = EciesEncryptor::new(public).encrypt_content(plaintext, [], None)?;
let wire = info.encrypted_content.as_ref().ok_or(EciesError::InvalidCiphertext)?.as_bytes();
let epk = ephemeral_pubkey_bytes::<Secp256k1EciesMessage>(wire)?;
let provider = Secp256k1KeyProvider::from(Secp256k1SigningKey::from(secret));
let shared = provider.key_agreement(epk).await?;
let decryptor = EciesSharedSecretDecryptor::<DefaultCryptoProvider>::new(shared);
let opened = decryptor.decrypt_content(&info)?.to_insecure()?;
assert_eq!(
&opened[..],
plaintext,
"provider key agreement must reproduce the ECIES plaintext"
);
Ok(())
}
}