use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use aes_gcm::aead::Aead;
use aes_gcm::{Aes256Gcm, KeyInit};
use hkdf::Hkdf;
use sha2::{Digest, Sha256};
use x25519_dalek::{PublicKey, StaticSecret};
pub const ENCRYPTION_SCHEME_X25519_AES256GCM: u8 = 1;
pub const AES_KEY_LEN: usize = 32;
pub const AES_GCM_NONCE_LEN: usize = 12;
pub const AES_GCM_TAG_LEN: usize = 16;
pub const X25519_PUBLIC_KEY_LEN: usize = 32;
pub const X25519_PRIVATE_KEY_LEN: usize = 32;
pub const RECIPIENT_KEY_ID_LEN: usize = 32;
const HKDF_INFO_KEY: &[u8] = b"keleusma-v1-aes256-gcm-key";
const HKDF_INFO_NONCE: &[u8] = b"keleusma-v1-aes256-gcm-nonce";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncryptionError {
KeyDerivationFailed(String),
EncryptFailed(String),
DecryptFailed(String),
WrongRecipient,
NonContributoryKey,
}
impl core::fmt::Display for EncryptionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::KeyDerivationFailed(s) => write!(f, "HKDF key derivation failed: {}", s),
Self::EncryptFailed(s) => write!(f, "AES-GCM encryption failed: {}", s),
Self::DecryptFailed(s) => write!(f, "AES-GCM decryption failed: {}", s),
Self::WrongRecipient => write!(f, "encrypted artefact is not for this recipient"),
Self::NonContributoryKey => {
write!(
f,
"ephemeral public key is a low-order (non-contributory) point"
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionMetadata {
pub scheme_id: u8,
pub ephemeral_public_key: [u8; X25519_PUBLIC_KEY_LEN],
pub recipient_key_id: [u8; RECIPIENT_KEY_ID_LEN],
pub aes_gcm_nonce: [u8; AES_GCM_NONCE_LEN],
}
impl EncryptionMetadata {
pub fn to_bytes(&self) -> [u8; 88] {
let mut buf = [0u8; 88];
buf[0] = self.scheme_id;
buf[1] = 0; buf[2..4].copy_from_slice(&88u16.to_le_bytes());
buf[4..36].copy_from_slice(&self.ephemeral_public_key);
buf[36..68].copy_from_slice(&self.recipient_key_id);
buf[68..80].copy_from_slice(&self.aes_gcm_nonce);
buf
}
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < 88 {
return None;
}
let scheme_id = buf[0];
if scheme_id != ENCRYPTION_SCHEME_X25519_AES256GCM {
return None;
}
let length = u16::from_le_bytes([buf[2], buf[3]]);
if length != 88 {
return None;
}
let mut ephemeral_public_key = [0u8; X25519_PUBLIC_KEY_LEN];
ephemeral_public_key.copy_from_slice(&buf[4..36]);
let mut recipient_key_id = [0u8; RECIPIENT_KEY_ID_LEN];
recipient_key_id.copy_from_slice(&buf[36..68]);
let mut aes_gcm_nonce = [0u8; AES_GCM_NONCE_LEN];
aes_gcm_nonce.copy_from_slice(&buf[68..80]);
Some(Self {
scheme_id,
ephemeral_public_key,
recipient_key_id,
aes_gcm_nonce,
})
}
}
pub fn recipient_key_id(public_key: &[u8; X25519_PUBLIC_KEY_LEN]) -> [u8; RECIPIENT_KEY_ID_LEN] {
let mut hasher = Sha256::new();
hasher.update(public_key);
let digest = hasher.finalize();
let mut out = [0u8; RECIPIENT_KEY_ID_LEN];
out.copy_from_slice(&digest);
out
}
pub fn encrypt_to_recipient(
plaintext: &[u8],
recipient_public_key: &[u8; X25519_PUBLIC_KEY_LEN],
rng_seed: &[u8; X25519_PRIVATE_KEY_LEN],
) -> Result<(EncryptionMetadata, Vec<u8>), EncryptionError> {
let ephemeral_private = StaticSecret::from(*rng_seed);
let ephemeral_public = PublicKey::from(&ephemeral_private);
let recipient_pk = PublicKey::from(*recipient_public_key);
let shared_secret = ephemeral_private.diffie_hellman(&recipient_pk);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut key = [0u8; AES_KEY_LEN];
hkdf.expand(HKDF_INFO_KEY, &mut key)
.map_err(|e| EncryptionError::KeyDerivationFailed(format!("{:?}", e)))?;
let mut nonce = [0u8; AES_GCM_NONCE_LEN];
hkdf.expand(HKDF_INFO_NONCE, &mut nonce)
.map_err(|e| EncryptionError::KeyDerivationFailed(format!("{:?}", e)))?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| EncryptionError::EncryptFailed(format!("{:?}", e)))?;
let ciphertext = cipher
.encrypt(aes_gcm::Nonce::from_slice(&nonce), plaintext)
.map_err(|e| EncryptionError::EncryptFailed(format!("{:?}", e)))?;
let metadata = EncryptionMetadata {
scheme_id: ENCRYPTION_SCHEME_X25519_AES256GCM,
ephemeral_public_key: ephemeral_public.to_bytes(),
recipient_key_id: recipient_key_id(recipient_public_key),
aes_gcm_nonce: nonce,
};
Ok((metadata, ciphertext))
}
pub fn decrypt_from_metadata(
metadata: &EncryptionMetadata,
ciphertext: &[u8],
local_private_key: &[u8; X25519_PRIVATE_KEY_LEN],
) -> Result<Vec<u8>, EncryptionError> {
let local_secret = StaticSecret::from(*local_private_key);
let local_public = PublicKey::from(&local_secret);
let local_fingerprint = recipient_key_id(local_public.as_bytes());
if local_fingerprint != metadata.recipient_key_id {
return Err(EncryptionError::WrongRecipient);
}
let ephemeral_pk = PublicKey::from(metadata.ephemeral_public_key);
let shared_secret = local_secret.diffie_hellman(&ephemeral_pk);
if !shared_secret.was_contributory() {
return Err(EncryptionError::NonContributoryKey);
}
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut key = [0u8; AES_KEY_LEN];
hkdf.expand(HKDF_INFO_KEY, &mut key)
.map_err(|e| EncryptionError::KeyDerivationFailed(format!("{:?}", e)))?;
let mut expected_nonce = [0u8; AES_GCM_NONCE_LEN];
hkdf.expand(HKDF_INFO_NONCE, &mut expected_nonce)
.map_err(|e| EncryptionError::KeyDerivationFailed(format!("{:?}", e)))?;
if expected_nonce != metadata.aes_gcm_nonce {
return Err(EncryptionError::DecryptFailed(String::from(
"nonce in metadata does not match HKDF derivation",
)));
}
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| EncryptionError::DecryptFailed(format!("{:?}", e)))?;
let plaintext = cipher
.decrypt(
aes_gcm::Nonce::from_slice(&metadata.aes_gcm_nonce),
ciphertext,
)
.map_err(|e| EncryptionError::DecryptFailed(format!("{:?}", e)))?;
Ok(plaintext)
}
pub fn public_key_from_private(
private_key: &[u8; X25519_PRIVATE_KEY_LEN],
) -> [u8; X25519_PUBLIC_KEY_LEN] {
let secret = StaticSecret::from(*private_key);
let public = PublicKey::from(&secret);
public.to_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_key(label: u8) -> [u8; 32] {
let mut k = [label; 32];
k[0] &= 248;
k[31] &= 127;
k[31] |= 64;
k
}
#[test]
fn round_trip_smallest() {
let recipient_sk = test_key(0x11);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = test_key(0x22);
let plaintext = b"hello, world";
let (metadata, ciphertext) =
encrypt_to_recipient(plaintext, &recipient_pk, &ephemeral_seed).expect("encrypt");
assert_eq!(
ciphertext.len(),
plaintext.len() + AES_GCM_TAG_LEN,
"ciphertext length should equal plaintext length plus tag"
);
let decrypted =
decrypt_from_metadata(&metadata, &ciphertext, &recipient_sk).expect("decrypt");
assert_eq!(decrypted, plaintext);
}
#[test]
fn low_order_ephemeral_key_is_rejected() {
let recipient_sk = test_key(0x55);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = test_key(0x66);
let (mut metadata, ciphertext) =
encrypt_to_recipient(b"secret", &recipient_pk, &ephemeral_seed).expect("encrypt");
metadata.ephemeral_public_key = [0u8; 32];
assert!(matches!(
decrypt_from_metadata(&metadata, &ciphertext, &recipient_sk),
Err(EncryptionError::NonContributoryKey)
));
}
#[test]
fn round_trip_large_payload() {
let recipient_sk = test_key(0x33);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = test_key(0x44);
let plaintext: Vec<u8> = (0..16384).map(|i| (i % 256) as u8).collect();
let (metadata, ciphertext) =
encrypt_to_recipient(&plaintext, &recipient_pk, &ephemeral_seed).expect("encrypt");
assert_eq!(ciphertext.len(), plaintext.len() + AES_GCM_TAG_LEN);
let decrypted =
decrypt_from_metadata(&metadata, &ciphertext, &recipient_sk).expect("decrypt");
assert_eq!(decrypted, plaintext);
}
#[test]
fn wrong_recipient_rejected() {
let alice_sk = test_key(0x55);
let alice_pk = public_key_from_private(&alice_sk);
let bob_sk = test_key(0x66);
let ephemeral_seed = test_key(0x77);
let (metadata, ciphertext) =
encrypt_to_recipient(b"for alice only", &alice_pk, &ephemeral_seed).expect("encrypt");
let result = decrypt_from_metadata(&metadata, &ciphertext, &bob_sk);
assert_eq!(result, Err(EncryptionError::WrongRecipient));
}
#[test]
fn tampered_ciphertext_rejected() {
let recipient_sk = test_key(0x88);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = test_key(0x99);
let plaintext = b"sensitive payload";
let (metadata, mut ciphertext) =
encrypt_to_recipient(plaintext, &recipient_pk, &ephemeral_seed).expect("encrypt");
ciphertext[3] ^= 0x01;
let result = decrypt_from_metadata(&metadata, &ciphertext, &recipient_sk);
assert!(
matches!(result, Err(EncryptionError::DecryptFailed(_))),
"expected decryption failure on tampered ciphertext, got {:?}",
result
);
}
#[test]
fn tampered_tag_rejected() {
let recipient_sk = test_key(0xaa);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = test_key(0xbb);
let plaintext = b"sensitive payload";
let (metadata, mut ciphertext) =
encrypt_to_recipient(plaintext, &recipient_pk, &ephemeral_seed).expect("encrypt");
let tag_offset = ciphertext.len() - 8;
ciphertext[tag_offset] ^= 0x01;
let result = decrypt_from_metadata(&metadata, &ciphertext, &recipient_sk);
assert!(matches!(result, Err(EncryptionError::DecryptFailed(_))));
}
#[test]
fn metadata_round_trip() {
let metadata = EncryptionMetadata {
scheme_id: ENCRYPTION_SCHEME_X25519_AES256GCM,
ephemeral_public_key: [0x42; 32],
recipient_key_id: [0x77; 32],
aes_gcm_nonce: [0xa5; 12],
};
let bytes = metadata.to_bytes();
assert_eq!(bytes.len(), 88);
let parsed = EncryptionMetadata::from_bytes(&bytes).expect("parse");
assert_eq!(parsed, metadata);
}
#[test]
fn metadata_rejects_unknown_scheme() {
let mut bytes = [0u8; 88];
bytes[0] = 99; bytes[2..4].copy_from_slice(&88u16.to_le_bytes());
assert!(EncryptionMetadata::from_bytes(&bytes).is_none());
}
#[test]
fn metadata_rejects_short_buffer() {
let bytes = [0u8; 40];
assert!(EncryptionMetadata::from_bytes(&bytes).is_none());
}
#[test]
fn recipient_key_id_is_sha256() {
let zeros = [0u8; 32];
let fingerprint = recipient_key_id(&zeros);
let expected: [u8; 32] = [
0x66, 0x68, 0x7a, 0xad, 0xf8, 0x62, 0xbd, 0x77, 0x6c, 0x8f, 0xc1, 0x8b, 0x8e, 0x9f,
0x8e, 0x20, 0x08, 0x97, 0x14, 0x85, 0x6e, 0xe2, 0x33, 0xb3, 0x90, 0x2a, 0x59, 0x1d,
0x0d, 0x5f, 0x29, 0x25,
];
assert_eq!(fingerprint, expected);
}
#[test]
fn different_ephemeral_keys_produce_different_ciphertexts() {
let recipient_sk = test_key(0xcc);
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_a = test_key(0xdd);
let ephemeral_b = test_key(0xee);
let plaintext = b"identical plaintext";
let (_, ciphertext_a) =
encrypt_to_recipient(plaintext, &recipient_pk, &ephemeral_a).expect("encrypt");
let (_, ciphertext_b) =
encrypt_to_recipient(plaintext, &recipient_pk, &ephemeral_b).expect("encrypt");
assert_ne!(
ciphertext_a, ciphertext_b,
"different ephemeral keys must produce different ciphertexts"
);
}
}