From 94f22805704f25005f2795cde96d07858b289c22 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:11:00 +0200 Subject: [PATCH] Basic Crypto --- codec/src/communication_types.rs | 18 +-- codec/src/communication_value.rs | 14 +- codec/src/data_value.rs | 131 +++++++++++------ common/src/lib.rs | 4 +- crypto/Cargo.toml | 17 ++- crypto/src/helper.rs | 235 +++++++++++++++++++++++++++++++ crypto/src/keypair.rs | 193 +++++++++++++++++++++++++ crypto/src/lib.rs | 17 ++- registry/src/lib.rs | 12 +- type-map/src/lib.rs | 6 +- 10 files changed, 568 insertions(+), 79 deletions(-) create mode 100644 crypto/src/helper.rs diff --git a/codec/src/communication_types.rs b/codec/src/communication_types.rs index 16a4f1d..1e7f4f7 100644 --- a/codec/src/communication_types.rs +++ b/codec/src/communication_types.rs @@ -4,21 +4,21 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(non_camel_case_types)] pub enum CommunicationType { - ping = 0x01, - message_send = 0x02, - update = 0x03, + ping = 0x0001, + message_send = 0x0002, + update = 0x0003, } impl CommunicationType { - pub fn as_number(self) -> u8 { - self as u8 + pub fn as_number(self) -> u16 { + self as u16 } - pub fn from_number(n: u8) -> Self { + pub fn from_number(n: u16) -> Self { match n { - 0x01 => CommunicationType::ping, - 0x02 => CommunicationType::message_send, - 0x03 => CommunicationType::update, + 0x0001 => CommunicationType::ping, + 0x0002 => CommunicationType::message_send, + 0x0003 => CommunicationType::update, _ => CommunicationType::ping, } } diff --git a/codec/src/communication_value.rs b/codec/src/communication_value.rs index 255617c..2a9300d 100644 --- a/codec/src/communication_value.rs +++ b/codec/src/communication_value.rs @@ -80,7 +80,7 @@ impl CommunicationValue { /* * Frame format (strict new format): * [4 bytes u32 total_length] // number of bytes after this field - * [1 byte communication_type] + * [2 bytes u16 communication_type] * [1 byte flags] * [optional 4 bytes id] // if flags bit2 set * [optional 6 bytes sender] // if flags bit0 set @@ -110,7 +110,7 @@ impl CommunicationValue { flags |= 0b0000_0100; } - payload.push(self.comm_type.0); + let _ = payload.write_u16::(self.comm_type.0); payload.push(flags); if has_id { @@ -148,7 +148,7 @@ impl CommunicationValue { let frame_end = 4 + total_len; - let comm_type_num = cursor.read_u8().ok()?; + let comm_type_num = cursor.read_u16::().ok()?; let comm_type = CommTypeId(comm_type_num); let flags = cursor.read_u8().ok()?; @@ -216,13 +216,13 @@ mod tests { let cv = CommunicationValue::new(CommTypeId(1)).with_id(0); let bytes = cv.to_bytes(); - // [u32 len][type][flags]... - assert!(bytes.len() >= 6); + // [u32 len][u16 type][flags]... + assert!(bytes.len() >= 7); let mut c = Cursor::new(bytes.as_slice()); let total_len = c.read_u32::().expect("read len"); assert_eq!(total_len as usize + 4, bytes.len()); - let typ = c.read_u8().expect("read type"); + let typ = c.read_u16::().expect("read type"); assert_eq!(typ, 1); let flags = c.read_u8().expect("read flags"); @@ -242,7 +242,7 @@ mod tests { let total_len = c.read_u32::().expect("len"); assert_eq!(total_len as usize + 4, bytes.len()); - let typ = c.read_u8().expect("type"); + let typ = c.read_u16::().expect("type"); assert_eq!(typ, 2); let flags = c.read_u8().expect("flags"); diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs index cd57d84..54c3421 100644 --- a/codec/src/data_value.rs +++ b/codec/src/data_value.rs @@ -7,6 +7,9 @@ use std::io::Cursor; use mtp_type_map::DataTypeId; +#[cfg(feature = "crypto")] +use mtp_crypto::{AeadDecrypt, AeadEncrypt}; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DataKind { Bool, @@ -44,7 +47,7 @@ pub enum DataValue { Container(Vec<(DataTypeId, DataValue)>), #[cfg(feature = "crypto")] - EncryptedContainer(Vec<(DataTypeId, DataValue)>), + EncryptedContainer(Vec), Null, } @@ -52,26 +55,36 @@ pub enum DataValue { impl DataValue { /* * Container format: - * [2 bytes u16 entry_count] // length of the container + * [2 bytes u16 entry_count] // number of entries * [1 byte kind] // DataValue kind marker * [if kind == BOOL_TRUE or BOOL_FALSE:] - * [1 byte key] // DataTypes discriminant + * [2 bytes u16 key] // DataTypeId discriminant * [else:] - * [2 bytes u16 payload_len] // length of the value payload - * [1 byte key] // DataTypes discriminant + * [4 bytes u32 payload_len] // length of the value payload + * [2 bytes u16 key] // DataTypeId discriminant * [payload_len bytes payload] // value data (interpreted based on kind) * + * Array format (same as container but no keys): + * [2 bytes u16 entry_count] + * for each entry: + * [1 byte kind] + * [if kind == BOOL_TRUE or BOOL_FALSE:] + * (no payload) + * [else:] + * [4 bytes u32 payload_len] + * [payload_len bytes payload] + * * Kind markers: * 0x01 => BoolTrue * 0x02 => BoolFalse - * 0x03 => Signed Number (i64, 8 bytes big-endian) - * 0x04 => Unsigned Number (u64, 8 bytes big-endian) - * 0x05 => Float (1 byte exponent, 3 bytes mantissa) + * 0x03 => Signed Number (i128, 16 bytes big-endian) + * 0x04 => Unsigned Number (u128, 16 bytes big-endian) + * 0x05 => Float (1 byte exponent, 4 bytes mantissa) * 0x06 => Str (UTF-8 bytes) * 0x07 => Bytes - * 0x08 => Array (nested container format) - * 0x09 => Container (nested container format) - * 0x0A => EncryptedContainer (nested container format) + * 0x08 => Array + * 0x09 => Container + * 0x0A => EncryptedContainer (4 bytes u32 len + encrypted bytes) * 0x0B => Null */ const KIND_BOOL_TRUE: u8 = 0x01; @@ -184,13 +197,49 @@ impl DataValue { } #[cfg(feature = "crypto")] - pub fn as_encrypted_container(&self) -> Option> { + pub fn as_encrypted_container(&self) -> Option> { match self { DataValue::EncryptedContainer(c) => Some(c.clone()), _ => None, } } + /// Decrypt an `EncryptedContainer` in-place, replacing it with the + /// deserialized `Container`. Returns `None` if decryption or + /// deserialization fails. + #[cfg(feature = "crypto")] + pub fn decrypt_into_container( + &mut self, + cipher: &impl AeadDecrypt, + aad: &[u8], + ) -> Option<()> { + let data = self.as_encrypted_container()?; + let plaintext = cipher.decrypt(&data, aad).ok()?; + let dv = DataValue::from_bytes(&plaintext)?; + match dv { + DataValue::Container(entries) => { + *self = DataValue::Container(entries); + Some(()) + } + _ => None, + } + } + + /// Encrypt a `Container` into an `EncryptedContainer` in-place. + /// Returns `None` if the value is not a `Container` or encryption fails. + #[cfg(feature = "crypto")] + pub fn encrypt_container( + &mut self, + cipher: &impl AeadEncrypt, + aad: &[u8], + ) -> Option<()> { + let entries = self.as_container()?; + let plaintext = DataValue::Container(entries).to_bytes(); + let ct = cipher.encrypt(&plaintext, aad).ok()?; + *self = DataValue::EncryptedContainer(ct); + Some(()) + } + pub fn as_map(&self) -> Option> { match self { DataValue::Container(c) => { @@ -258,8 +307,7 @@ impl DataValue { buf.push(kind); if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE { - let key = u8::try_from(key.0).unwrap_or(0); - buf.push(key); + let _ = buf.write_u16::(key.0); return true; } @@ -268,16 +316,10 @@ impl DataValue { return false; } - let len_u16 = match u16::try_from(payload.len()) { - Ok(v) => v, - Err(_) => return false, - }; - - if buf.write_u16::(len_u16).is_err() { + if buf.write_u32::(payload.len() as u32).is_err() { return false; } - let key = u8::try_from(key.0).unwrap_or(0); - buf.push(key); + let _ = buf.write_u16::(key.0); buf.extend_from_slice(&payload); true } @@ -313,12 +355,7 @@ impl DataValue { return false; } - let len_u16 = match u16::try_from(payload.len()) { - Ok(v) => v, - Err(_) => return false, - }; - - if buf.write_u16::(len_u16).is_err() { + if buf.write_u32::(payload.len() as u32).is_err() { return false; } buf.extend_from_slice(&payload); @@ -368,9 +405,8 @@ impl DataValue { Some(()) } #[cfg(feature = "crypto")] - DataValue::EncryptedContainer(entries) => { - let bytes = Self::encode_container(entries); - buf.extend_from_slice(&bytes); + DataValue::EncryptedContainer(data) => { + buf.extend_from_slice(data); Some(()) } @@ -418,7 +454,7 @@ impl DataValue { continue; } - let len = cursor.read_u16::().ok()? as usize; + let len = cursor.read_u32::().ok()? as usize; let key = DataTypeId(cursor.read_u16::().ok()?); let start = cursor.position() as usize; @@ -457,7 +493,7 @@ impl DataValue { continue; } - let len = cursor.read_u16::().ok()? as usize; + let len = cursor.read_u32::().ok()? as usize; let start = cursor.position() as usize; let end = start.checked_add(len)?; if end > cursor.get_ref().len() { @@ -510,6 +546,17 @@ impl DataValue { cursor.set_position(end as u64); Some(DataValue::Str(s)) } + Self::KIND_BYTES => { + let len = payload_len?; + let start = cursor.position() as usize; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + let b = cursor.get_ref()[start..end].to_vec(); + cursor.set_position(end as u64); + Some(DataValue::Bytes(b)) + } Self::KIND_ARRAY => { let start = cursor.position() as usize; let len = payload_len?; @@ -542,25 +589,19 @@ impl DataValue { } #[cfg(feature = "crypto")] Self::KIND_ENCRYPTED_CONTAINER => { - let start = cursor.position() as usize; let len = payload_len?; + let start = cursor.position() as usize; let end = start.checked_add(len)?; if end > cursor.get_ref().len() { return None; } - let mut inner = Cursor::new(&cursor.get_ref()[start..end]); - let c = Self::try_read_container(&mut inner)?; - if inner.position() as usize != len { - return None; - } + let data = cursor.get_ref()[start..end].to_vec(); cursor.set_position(end as u64); - if let DataValue::Container(entries) = c { - Some(DataValue::EncryptedContainer(entries)) - } else { - None - } + Some(DataValue::EncryptedContainer(data)) } Self::KIND_NULL => Some(DataValue::Null), + #[cfg(not(feature = "crypto"))] + 0x0A => None, _ => None, } } @@ -660,7 +701,7 @@ impl Hash for DataValue { c.hash(state); } Null => { - 6u8.hash(state); + 7u8.hash(state); } } } diff --git a/common/src/lib.rs b/common/src/lib.rs index 0ec7f70..86471f9 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,7 +1,7 @@ use thiserror::Error; pub enum RegistryError { - ReservedCommId(u8, String), + ReservedCommId(u16, String), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -9,7 +9,7 @@ pub enum CodecError { UnknownVersion, UnknownCommunicationType(String), UnknownDataType(String), - ReservedCommunicationType(u8), + ReservedCommunicationType(u16), InvalidEncoding, } diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index bb3004e..9d7dc79 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -6,16 +6,21 @@ edition = "2024" [dependencies] chacha20poly1305 = { version = "0.10", optional = true } aes-gcm = { version = "0.10", optional = true } -ed25519-dalek = { version = "2.1", optional = true, features = ["pkcs8", "pem"] } -hkdf = { version = "0.12", optional = true } -sha2 = { version = "0.10", optional = true } -zeroize = { version = "1.7", features = ["derive"] } +ed25519-dalek = { version = "2.2", optional = true, features = [ + "pkcs8", + "pem", +] } +hkdf = { version = "0.13", optional = true } +sha2 = { version = "0.11", optional = true } +zeroize = { version = "1.9", features = ["derive"] } rand_core = { version = "0.6", features = ["getrandom"] } -getrandom = "0.2" +getrandom = "0.4.3" mlkem-tls = { version = "0.2", optional = true } -ml-dsa = { version = "0.0.4", optional = true } +ml-dsa = { version = "0.1.1", optional = true } +serde = { version = "1", optional = true, features = ["derive"] } [features] default = ["chacha20poly1305", "ed25519-dalek", "hkdf", "sha2"] full = ["chacha20poly1305", "aes-gcm", "ed25519-dalek", "hkdf", "sha2"] pqc = ["mlkem-tls", "ml-dsa"] +serde = ["dep:serde"] diff --git a/crypto/src/helper.rs b/crypto/src/helper.rs new file mode 100644 index 0000000..46e399a --- /dev/null +++ b/crypto/src/helper.rs @@ -0,0 +1,235 @@ +use crate::error::CryptoError; + +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +use crate::aead::{AeadDecrypt, AeadEncrypt, ChaCha20Poly1305}; +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +use crate::kdf::derive_encryption_key; +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +use crate::kem::HybridKem; +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +use crate::keypair::{Keyring, PublicKeyBundle}; +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +use rand_core::RngCore; + +pub struct RecipientEntry { + pub kem_ciphertext: Vec, + pub encrypted_key: Vec, +} + +/* + * A payload encrypted for multiple recipients. + * + * Any recipient who possesses the corresponding `KemPrivateKey` can decrypt the message. + */ +pub struct MultiEncryptedMessage { + pub recipients: Vec, + pub nonce: [u8; 24], + pub ciphertext: Vec, +} + +impl MultiEncryptedMessage { + /* + * Serialize into a compact byte vector. + * + * Format: + * - `num_recipients: u16` + * - for each recipient: + * - `kem_ct_len: u16` | `kem_ciphertext` + * - `ek_len: u16` | `encrypted_key` + * - `nonce: 24 bytes` + * - `ciphertext` (remaining) + */ + pub fn to_bytes(&self) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(self.recipients.len() as u16).to_be_bytes()); + for r in &self.recipients { + out.extend_from_slice(&(r.kem_ciphertext.len() as u16).to_be_bytes()); + out.extend_from_slice(&r.kem_ciphertext); + out.extend_from_slice(&(r.encrypted_key.len() as u16).to_be_bytes()); + out.extend_from_slice(&r.encrypted_key); + } + out.extend_from_slice(&self.nonce); + out.extend_from_slice(&self.ciphertext); + out + } + + /// Deserialize from bytes produced by `to_bytes`. + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut offset = 0; + let read_u16 = |off: &mut usize| -> Result { + let v = u16::from_be_bytes( + bytes + .get(*off..*off + 2) + .ok_or(CryptoError::DecryptionFailed)? + .try_into() + .unwrap(), + ); + *off += 2; + Ok(v) + }; + + let num = read_u16(&mut offset)? as usize; + let mut recipients = Vec::with_capacity(num); + for _ in 0..num { + let klen = read_u16(&mut offset)? as usize; + let kem_ct = bytes + .get(offset..offset + klen) + .ok_or(CryptoError::DecryptionFailed)? + .to_vec(); + offset += klen; + + let elen = read_u16(&mut offset)? as usize; + let enc_key = bytes + .get(offset..offset + elen) + .ok_or(CryptoError::DecryptionFailed)? + .to_vec(); + offset += elen; + + recipients.push(RecipientEntry { + kem_ciphertext: kem_ct, + encrypted_key: enc_key, + }); + } + + let nonce: [u8; 24] = bytes + .get(offset..offset + 24) + .ok_or(CryptoError::DecryptionFailed)? + .try_into() + .map_err(|_| CryptoError::DecryptionFailed)?; + offset += 24; + + let ciphertext = bytes + .get(offset..) + .ok_or(CryptoError::DecryptionFailed)? + .to_vec(); + + Ok(Self { + recipients, + nonce, + ciphertext, + }) + } +} + +/* + * Encrypt `plaintext` for every recipient in `entities`. + * + * Internally generates a fresh content-encryption key, encrypts the payload + * with ChaCha20-Poly1305, then KEM-encapsulates and wraps the key for each + * recipient. The returned `MultiEncryptedMessage` can be decrypted by any + * entity whose keyring contains the corresponding private KEM key. + * + * Requires the `pqc` and `chacha20poly1305` features. + */ +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +pub fn encrypt_multi( + plaintext: &[u8], + aad: &[u8], + entities: &[PublicKeyBundle], +) -> Result { + let mut cek = [0u8; 32]; + rand_core::OsRng.fill_bytes(&mut cek); + + let cipher = ChaCha20Poly1305::new(cek); + let encrypted_payload = cipher.encrypt(plaintext, aad)?; + + let nonce: [u8; 24] = encrypted_payload[..24] + .try_into() + .map_err(|_| CryptoError::EncryptionFailed)?; + let ciphertext = encrypted_payload[24..].to_vec(); + + let mut recipients = Vec::with_capacity(entities.len()); + for entity in entities { + let enc = HybridKem::encapsulate(&entity.kem_public_key)?; + let wrap_key = derive_encryption_key( + &enc.shared_secret, + b"mtp-multi-key-wrap", + b"multi-recipient", + )?; + + let wrap_cipher = ChaCha20Poly1305::new(wrap_key); + let encrypted_key = wrap_cipher.encrypt(&cek, b"")?; + + recipients.push(RecipientEntry { + kem_ciphertext: enc.ciphertext, + encrypted_key, + }); + } + + Ok(MultiEncryptedMessage { + recipients, + nonce, + ciphertext, + }) +} + +/* + * Decrypt a `MultiEncryptedMessage` using the recipient's `Keyring`. + * + * Tries each `RecipientEntry` until one succeeds with the given keyring's + * KEM secret key. Returns the original plaintext. + */ +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +pub fn decrypt_multi( + msg: &MultiEncryptedMessage, + aad: &[u8], + keyring: &Keyring, +) -> Result, CryptoError> { + for entry in &msg.recipients { + let ss = match HybridKem::decapsulate(&keyring.kem_secret_key, &entry.kem_ciphertext) { + Ok(s) => s, + Err(_) => continue, + }; + let wrap_key = derive_encryption_key(&ss, b"mtp-multi-key-wrap", b"multi-recipient")?; + let wrap_cipher = ChaCha20Poly1305::new(wrap_key); + let cek = match wrap_cipher.decrypt(&entry.encrypted_key, b"") { + Ok(k) => k, + Err(_) => continue, + }; + let cek_arr: [u8; 32] = cek.try_into().map_err(|_| CryptoError::DecryptionFailed)?; + + let mut full_ct = Vec::with_capacity(24 + msg.ciphertext.len()); + full_ct.extend_from_slice(&msg.nonce); + full_ct.extend_from_slice(&msg.ciphertext); + + let data_cipher = ChaCha20Poly1305::new(cek_arr); + return data_cipher.decrypt(&full_ct, aad); + } + Err(CryptoError::DecryptionFailed) +} + +/// Verify an Ed25519 signature against a public key. +#[cfg(feature = "ed25519-dalek")] +pub fn verify_ed25519_sig( + public_key: &crate::keypair::SignaturePublicKey, + msg: &[u8], + signature: &[u8], +) -> Result<(), CryptoError> { + crate::sign::verify_ed25519(public_key, msg, signature) +} + +/// Verify an ML-DSA signature against a public key. +#[cfg(feature = "ml-dsa")] +pub fn verify_ml_dsa_sig( + public_key: &crate::keypair::SignaturePqPublicKey, + msg: &[u8], + signature: &[u8], +) -> Result<(), CryptoError> { + crate::sign::verify_ml_dsa(public_key, msg, signature) +} + +/* + * Verify both an Ed25519 and ML-DSA signature (dual) against + * the public keys in a `PublicKeyBundle`. + */ +#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))] +pub fn verify_dual_sig( + public_keys: &crate::keypair::PublicKeyBundle, + msg: &[u8], + ed25519_sig: &[u8], + mldsa_sig: &[u8], +) -> Result<(), CryptoError> { + verify_ed25519_sig(&public_keys.sig_cl_public_key, msg, ed25519_sig)?; + verify_ml_dsa_sig(&public_keys.sig_pq_public_key, msg, mldsa_sig)?; + Ok(()) +} diff --git a/crypto/src/keypair.rs b/crypto/src/keypair.rs index 0fcb29f..7e928a1 100644 --- a/crypto/src/keypair.rs +++ b/crypto/src/keypair.rs @@ -1,5 +1,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] pub struct EncryptionPrivateKey(Vec); @@ -19,6 +21,8 @@ impl From> for EncryptionPrivateKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] pub struct SignaturePrivateKey(Vec); @@ -38,6 +42,8 @@ impl From> for SignaturePrivateKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] pub struct EncryptionPublicKey(Vec); @@ -57,6 +63,8 @@ impl From> for EncryptionPublicKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] pub struct SignaturePublicKey(Vec); @@ -102,6 +110,8 @@ impl KeyGroup { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] pub struct KemPrivateKey(Vec); @@ -121,6 +131,8 @@ impl From> for KemPrivateKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] pub struct KemPublicKey(Vec); @@ -140,6 +152,8 @@ impl From> for KemPublicKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] pub struct SignaturePqPublicKey(Vec); @@ -159,6 +173,8 @@ impl From> for SignaturePqPublicKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] pub struct SignaturePqPrivateKey(Vec); @@ -178,6 +194,7 @@ impl From> for SignaturePqPrivateKey { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(ZeroizeOnDrop)] pub struct Keyring { #[zeroize(skip)] @@ -209,4 +226,180 @@ impl Keyring { sig_cl_secret_key, } } + + #[cfg(all(feature = "mlkem-tls", feature = "ml-dsa", feature = "ed25519-dalek"))] + pub fn generate() -> Self { + let (kem_sk, kem_pk) = crate::kem::HybridKem::generate_keypair(); + + let (ed_signer, sig_cl_sk, sig_cl_pk) = crate::sign::Ed25519Signer::generate(); + let (_ml_signer, sig_pq_sk, sig_pq_pk) = crate::sign::MlDsaSigner::generate(); + + drop(ed_signer); + + Self { + kem_public_key: kem_pk, + kem_secret_key: kem_sk, + sig_pq_public_key: sig_pq_pk, + sig_pq_secret_key: sig_pq_sk, + sig_cl_public_key: sig_cl_pk, + sig_cl_secret_key: sig_cl_sk, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct PublicKeyBundle { + pub kem_public_key: KemPublicKey, + pub sig_pq_public_key: SignaturePqPublicKey, + pub sig_cl_public_key: SignaturePublicKey, +} + +impl PublicKeyBundle { + pub fn new( + kem_public_key: KemPublicKey, + sig_pq_public_key: SignaturePqPublicKey, + sig_cl_public_key: SignaturePublicKey, + ) -> Self { + Self { + kem_public_key, + sig_pq_public_key, + sig_cl_public_key, + } + } + + pub fn as_bytes(&self) -> Vec { + let kem = self.kem_public_key.as_bytes(); + let pq = self.sig_pq_public_key.as_bytes(); + let cl = self.sig_cl_public_key.as_bytes(); + + let mut out = Vec::with_capacity(kem.len() + pq.len() + cl.len() + 6); + out.extend_from_slice(&(kem.len() as u16).to_be_bytes()); + out.extend_from_slice(kem); + out.extend_from_slice(&(pq.len() as u16).to_be_bytes()); + out.extend_from_slice(pq); + out.extend_from_slice(&(cl.len() as u16).to_be_bytes()); + out.extend_from_slice(cl); + out + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + use crate::error::CryptoError; + let mut offset = 0; + + let kem_len = u16::from_be_bytes( + bytes + .get(offset..offset + 2) + .ok_or(CryptoError::InvalidKeyLength)? + .try_into() + .unwrap(), + ) as usize; + offset += 2; + let kem = KemPublicKey::new( + bytes + .get(offset..offset + kem_len) + .ok_or(CryptoError::InvalidKeyLength)? + .to_vec(), + ); + offset += kem_len; + + let pq_len = u16::from_be_bytes( + bytes + .get(offset..offset + 2) + .ok_or(CryptoError::InvalidKeyLength)? + .try_into() + .unwrap(), + ) as usize; + offset += 2; + let pq = SignaturePqPublicKey::new( + bytes + .get(offset..offset + pq_len) + .ok_or(CryptoError::InvalidKeyLength)? + .to_vec(), + ); + offset += pq_len; + + let cl_len = u16::from_be_bytes( + bytes + .get(offset..offset + 2) + .ok_or(CryptoError::InvalidKeyLength)? + .try_into() + .unwrap(), + ) as usize; + offset += 2; + let cl = SignaturePublicKey::new( + bytes + .get(offset..offset + cl_len) + .ok_or(CryptoError::InvalidKeyLength)? + .to_vec(), + ); + + Ok(Self { + kem_public_key: kem, + sig_pq_public_key: pq, + sig_cl_public_key: cl, + }) + } +} + +impl Keyring { + pub fn public_key_bundle(&self) -> PublicKeyBundle { + PublicKeyBundle { + kem_public_key: self.kem_public_key.clone(), + sig_pq_public_key: self.sig_pq_public_key.clone(), + sig_cl_public_key: self.sig_cl_public_key.clone(), + } + } + + /// Serialize the full keyring (all six keys) into a byte vector. + /// + /// Format: for each key, a 2-byte length prefix followed by the key bytes, + /// in the order: kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_cl_pk, sig_cl_sk. + pub fn to_bytes(&self) -> Vec { + let fields: &[&[u8]] = &[ + self.kem_public_key.as_bytes(), + self.kem_secret_key.as_bytes(), + self.sig_pq_public_key.as_bytes(), + self.sig_pq_secret_key.as_bytes(), + self.sig_cl_public_key.as_bytes(), + self.sig_cl_secret_key.as_bytes(), + ]; + let mut out = Vec::new(); + for f in fields { + out.extend_from_slice(&(f.len() as u16).to_be_bytes()); + out.extend_from_slice(f); + } + out + } + + /// Deserialize a full keyring from bytes produced by [`Keyring::to_bytes`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + use crate::error::CryptoError; + let mut offset = 0; + let read_key = |offset: &mut usize| -> Result, CryptoError> { + let len = u16::from_be_bytes( + bytes + .get(*offset..*offset + 2) + .ok_or(CryptoError::InvalidKeyLength)? + .try_into() + .unwrap(), + ) as usize; + *offset += 2; + let key = bytes + .get(*offset..*offset + len) + .ok_or(CryptoError::InvalidKeyLength)? + .to_vec(); + *offset += len; + Ok(key) + }; + + Ok(Self { + kem_public_key: KemPublicKey::new(read_key(&mut offset)?), + kem_secret_key: KemPrivateKey::new(read_key(&mut offset)?), + sig_pq_public_key: SignaturePqPublicKey::new(read_key(&mut offset)?), + sig_pq_secret_key: SignaturePqPrivateKey::new(read_key(&mut offset)?), + sig_cl_public_key: SignaturePublicKey::new(read_key(&mut offset)?), + sig_cl_secret_key: SignaturePrivateKey::new(read_key(&mut offset)?), + }) + } } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index d726960..8bbc4f7 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -14,11 +14,14 @@ pub mod sign; #[cfg(feature = "mlkem-tls")] pub mod kem; +pub mod helper; + pub use aead::{AeadCipher, AeadDecrypt, AeadEncrypt}; pub use error::CryptoError; pub use keypair::{ EncryptionPrivateKey, EncryptionPublicKey, KemPrivateKey, KemPublicKey, KeyGroup, Keyring, - SignaturePqPrivateKey, SignaturePqPublicKey, SignaturePrivateKey, SignaturePublicKey, + PublicKeyBundle, SignaturePqPrivateKey, SignaturePqPublicKey, SignaturePrivateKey, + SignaturePublicKey, }; #[cfg(feature = "chacha20poly1305")] @@ -44,3 +47,15 @@ pub use kdf::{derive_encryption_key, hkdf_expand, hkdf_extract}; #[cfg(feature = "mlkem-tls")] pub use kem::HybridKem; + +#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))] +pub use helper::{decrypt_multi, encrypt_multi, MultiEncryptedMessage, RecipientEntry}; + +#[cfg(feature = "ed25519-dalek")] +pub use helper::verify_ed25519_sig; + +#[cfg(feature = "ml-dsa")] +pub use helper::verify_ml_dsa_sig; + +#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))] +pub use helper::verify_dual_sig; diff --git a/registry/src/lib.rs b/registry/src/lib.rs index bd007b0..f2ab8da 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; #[repr(transparent)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommTypeId(pub u8); +pub struct CommTypeId(pub u16); #[repr(transparent)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -15,7 +15,7 @@ pub struct DataTypeId(pub u16); * across all versions for version negotiation & security. */ -pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..16; +pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..16; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum InternalCommType { @@ -27,7 +27,7 @@ pub enum InternalCommType { impl InternalCommType { pub fn as_id(self) -> CommTypeId { - CommTypeId(self as u8) + CommTypeId(self as u16) } } @@ -37,15 +37,15 @@ pub struct Version(pub u16, pub u16, pub u16); pub struct Registry { pub version: Version, - comm_name_to_id: HashMap, - comm_id_to_name: HashMap, + comm_name_to_id: HashMap, + comm_id_to_name: HashMap, data_name_to_id: HashMap, data_id_to_name: HashMap, } pub struct RegistryConfig { pub version: Version, - pub communication_types: HashMap, + pub communication_types: HashMap, pub data_types: HashMap, } diff --git a/type-map/src/lib.rs b/type-map/src/lib.rs index ddba981..e054034 100644 --- a/type-map/src/lib.rs +++ b/type-map/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommTypeId(pub u8); +pub struct CommTypeId(pub u16); #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct DataTypeId(pub u16); @@ -14,12 +14,12 @@ pub struct Version(pub u16); #[derive(Clone, Debug)] pub struct TypeMap { pub version: Version, - pub comm_types: HashMap, + pub comm_types: HashMap, pub data_types: HashMap, } impl TypeMap { - pub fn comm_id(&self, name: &str) -> Option { + pub fn comm_id(&self, name: &str) -> Option { self.comm_types.get(name).copied() } pub fn data_id(&self, name: &str) -> Option {