[Add] Ease of use functions
Some checks failed
CI / rustfmt (push) Failing after 17s
CI / wasm build (push) Failing after 1m13s
CI / clippy (push) Failing after 1m17s
CI / example (push) Failing after 1m30s
CI / test (push) Successful in 1m50s
CI / duplicate code (push) Failing after 31s
CI / web client (push) Failing after 31s
CI / cargo-machete (push) Successful in 1m15s
CI / cargo-deny (push) Failing after 2m26s

This commit is contained in:
Alex Emmet 2026-06-28 03:26:07 +02:00
commit 6ef1293603
15 changed files with 1203 additions and 124 deletions

2
Cargo.lock generated
View file

@ -999,6 +999,7 @@ dependencies = [
"mtp-crypto", "mtp-crypto",
"mtp-transport", "mtp-transport",
"rand 0.8.6", "rand 0.8.6",
"tokio",
] ]
[[package]] [[package]]
@ -1049,6 +1050,7 @@ dependencies = [
"mtp-crypto", "mtp-crypto",
"mtp-transport", "mtp-transport",
"rand 0.8.6", "rand 0.8.6",
"tokio",
] ]
[[package]] [[package]]

View file

@ -9,6 +9,7 @@ mtp-codec = { path = "../codec" }
mtp-transport = { path = "../transport" } mtp-transport = { path = "../transport" }
mtp-crypto = { path = "../crypto", optional = true } mtp-crypto = { path = "../crypto", optional = true }
rand = "0.8" rand = "0.8"
tokio = { version = "1", features = ["time"] }
[features] [features]
crypto = ["dep:mtp-crypto", "mtp-codec/crypto"] crypto = ["dep:mtp-crypto", "mtp-codec/crypto"]

View file

@ -1,6 +1,7 @@
use mtp_codec::{CommunicationValue, DataType, DataValue, PROTOCOL_VERSION, Version}; use mtp_codec::{CommunicationValue, DataType, DataValue, PROTOCOL_VERSION, Version};
use mtp_common::CommunicationError; use mtp_common::CommunicationError;
use mtp_transport::{Policy, Receiver, Sender}; use mtp_transport::{Policy, Receiver, Sender};
use tokio::time::Duration;
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
fn unexpected_response_type_error( fn unexpected_response_type_error(
@ -20,6 +21,8 @@ pub struct ClientConfig {
pub url: String, pub url: String,
pub tls: ClientTlsConfig, pub tls: ClientTlsConfig,
pub client_id: u64, pub client_id: u64,
#[cfg(feature = "crypto")]
pub auth_timeout: Duration,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -34,6 +37,8 @@ impl ClientConfig {
url: url.into(), url: url.into(),
tls: ClientTlsConfig::SystemRoots, tls: ClientTlsConfig::SystemRoots,
client_id: 0, client_id: 0,
#[cfg(feature = "crypto")]
auth_timeout: Duration::from_secs(30),
} }
} }
@ -51,6 +56,12 @@ impl ClientConfig {
self self
} }
#[cfg(feature = "crypto")]
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
self.auth_timeout = timeout;
self
}
fn server_cert(&self) -> Option<Vec<u8>> { fn server_cert(&self) -> Option<Vec<u8>> {
match &self.tls { match &self.tls {
ClientTlsConfig::SystemRoots => None, ClientTlsConfig::SystemRoots => None,
@ -73,6 +84,7 @@ pub struct MTPConnection {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthState { pub enum AuthState {
Unauthenticated,
Pending, Pending,
Authenticated, Authenticated,
Failed, Failed,
@ -91,7 +103,6 @@ impl MTPClient {
let (sender, receiver) = let (sender, receiver) =
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?; mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
// Build the initial identification message with the protocol version.
let version_str = format!("{}", PROTOCOL_VERSION); let version_str = format!("{}", PROTOCOL_VERSION);
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
.add_typed_default(DataType::Version, DataValue::Str(version_str)) .add_typed_default(DataType::Version, DataValue::Str(version_str))
@ -107,7 +118,7 @@ impl MTPClient {
sender, sender,
receiver, receiver,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_state: AuthState::Authenticated, auth_state: AuthState::Unauthenticated,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
client_id: config.client_id, client_id: config.client_id,
}) })
@ -126,14 +137,13 @@ impl MTPClient {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
fn verify_host_challenge( fn verify_host_challenge(
challenge: &CommunicationValue, challenge: &CommunicationValue,
tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle, host_pk: &mtp_crypto::PublicKeyBundle,
id: u64, id: u64,
server_challenge: u128, server_challenge: u128,
) -> Result<(), CommunicationError> { ) -> Result<(), CommunicationError> {
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa}; use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
let sig = match challenge.get_data(DataType::Signature.to_id(tm)) { let sig = match challenge.get_data(DataType::Signature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => { _ => {
return Err(CommunicationError::AuthenticationFailed( return Err(CommunicationError::AuthenticationFailed(
@ -141,7 +151,7 @@ fn verify_host_challenge(
)); ));
} }
}; };
let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) { let pq_sig = match challenge.get_data(DataType::PqSignature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
@ -165,7 +175,6 @@ fn verify_host_challenge(
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
fn verify_host_final( fn verify_host_final(
response: &CommunicationValue, response: &CommunicationValue,
tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle, host_pk: &mtp_crypto::PublicKeyBundle,
id: u64, id: u64,
client_nonce: u128, client_nonce: u128,
@ -173,7 +182,7 @@ fn verify_host_final(
) -> Result<(), CommunicationError> { ) -> Result<(), CommunicationError> {
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa}; use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
match response.get_data(DataType::ClientNonce.to_id(tm)) { match response.get_data(DataType::ClientNonce) {
DataValue::UnsignedNumber(n) if *n == client_nonce => {} DataValue::UnsignedNumber(n) if *n == client_nonce => {}
_ => { _ => {
return Err(CommunicationError::AuthenticationFailed( return Err(CommunicationError::AuthenticationFailed(
@ -182,7 +191,7 @@ fn verify_host_final(
} }
} }
let sig = match response.get_data(DataType::Signature.to_id(tm)) { let sig = match response.get_data(DataType::Signature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => { _ => {
return Err(CommunicationError::AuthenticationFailed( return Err(CommunicationError::AuthenticationFailed(
@ -190,7 +199,7 @@ fn verify_host_final(
)); ));
} }
}; };
let pq_sig = match response.get_data(DataType::PqSignature.to_id(tm)) { let pq_sig = match response.get_data(DataType::PqSignature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
@ -210,10 +219,9 @@ fn verify_host_final(
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
fn check_connected( fn check_connected(
response: &CommunicationValue, response: &CommunicationValue,
tm: &mtp_codec::TypeMap,
reject_msg: &str, reject_msg: &str,
) -> Result<(), CommunicationError> { ) -> Result<(), CommunicationError> {
match response.get_data(DataType::Connected.to_id(tm)) { match response.get_data(DataType::Connected) {
DataValue::BoolTrue => Ok(()), DataValue::BoolTrue => Ok(()),
DataValue::BoolFalse => Err(CommunicationError::AuthenticationFailed(reject_msg.into())), DataValue::BoolFalse => Err(CommunicationError::AuthenticationFailed(reject_msg.into())),
_ => Err(CommunicationError::AuthenticationFailed( _ => Err(CommunicationError::AuthenticationFailed(
@ -271,7 +279,7 @@ async fn receive_verified_challenge(
)); ));
} }
let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(tm)) { let server_challenge = match challenge.get_data(DataType::ServerNonce) {
DataValue::UnsignedNumber(n) => *n, DataValue::UnsignedNumber(n) => *n,
_ => { _ => {
return Err(CommunicationError::AuthenticationFailed( return Err(CommunicationError::AuthenticationFailed(
@ -280,13 +288,7 @@ async fn receive_verified_challenge(
} }
}; };
verify_host_challenge( verify_host_challenge(&challenge, host_public_key_bundle, bound_id, server_challenge)?;
&challenge,
tm,
host_public_key_bundle,
bound_id,
server_challenge,
)?;
Ok(server_challenge) Ok(server_challenge)
} }
@ -297,6 +299,25 @@ impl MTPClient {
config: ClientConfig, config: ClientConfig,
keys: &mtp_crypto::Keyring, keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle, host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> {
let timeout = config.auth_timeout;
match tokio::time::timeout(
timeout,
Self::auth_connect_inner(config, keys, host_public_key_bundle),
)
.await
{
Ok(result) => result,
Err(_) => Err(CommunicationError::AuthenticationFailed(
"authentication timed out".into(),
)),
}
}
async fn auth_connect_inner(
config: ClientConfig,
keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> { ) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::auth; use mtp_crypto::auth;
@ -313,17 +334,27 @@ impl MTPClient {
DataType::Id, DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128), DataValue::UnsignedNumber(config.client_id as u128),
); );
sender.send(&ident).await?; if let Err(e) = sender.send(&ident).await {
sender.close();
return Err(e);
}
// 2. Receive and verify the host's challenge. // 2. Receive and verify the host's challenge.
let server_challenge = receive_verified_challenge( let server_challenge = match receive_verified_challenge(
&receiver, &receiver,
&tm, &tm,
host_public_key_bundle, host_public_key_bundle,
config.client_id, config.client_id,
"auth_connect challenge", "auth_connect challenge",
) )
.await?; .await
{
Ok(c) => c,
Err(e) => {
sender.close();
return Err(e);
}
};
// 3. Sign the host's challenge and send the proof. // 3. Sign the host's challenge and send the proof.
let client_nonce: u128 = rand::random(); let client_nonce: u128 = rand::random();
@ -334,28 +365,49 @@ impl MTPClient {
client_nonce, client_nonce,
); );
let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?; let proof = match signed_challenge_response(keys, &proof_payload, client_nonce) {
sender.send(&proof).await?; Ok(p) => p,
Err(e) => {
sender.close();
return Err(e);
}
};
if let Err(e) = sender.send(&proof).await {
sender.close();
return Err(e);
}
// 4. Receive and verify the host's final confirmation. // 4. Receive and verify the host's final confirmation.
let response = receiver.receive().await?; let response = match receiver.receive().await {
Ok(r) => r,
Err(e) => {
sender.close();
return Err(e);
}
};
let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm); let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm);
if response.get_type() != expected_type { if response.get_type() != expected_type {
sender.close();
return Err(unexpected_response_type_error( return Err(unexpected_response_type_error(
"auth_connect", "auth_connect",
expected_type, expected_type,
&response, &response,
)); ));
} }
check_connected(&response, &tm, "Server rejected authentication")?; if let Err(e) = check_connected(&response, "Server rejected authentication") {
verify_host_final( sender.close();
return Err(e);
}
if let Err(e) = verify_host_final(
&response, &response,
&tm,
host_public_key_bundle, host_public_key_bundle,
config.client_id, config.client_id,
client_nonce, client_nonce,
server_challenge, server_challenge,
)?; ) {
sender.close();
return Err(e);
}
Ok(MTPConnection { Ok(MTPConnection {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
@ -370,6 +422,25 @@ impl MTPClient {
config: ClientConfig, config: ClientConfig,
keys: &mtp_crypto::Keyring, keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle, host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> {
let timeout = config.auth_timeout;
match tokio::time::timeout(
timeout,
Self::auth_register_inner(config, keys, host_public_key_bundle),
)
.await
{
Ok(result) => result,
Err(_) => Err(CommunicationError::AuthenticationFailed(
"authentication timed out".into(),
)),
}
}
async fn auth_register_inner(
config: ClientConfig,
keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> { ) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::auth; use mtp_crypto::auth;
@ -385,54 +456,86 @@ impl MTPClient {
let register = CommunicationValue::new(mtp_codec::CommunicationType::Register) let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone())) .add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone())); .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
sender.send(&register).await?; if let Err(e) = sender.send(&register).await {
sender.close();
return Err(e);
}
// 2. Receive and verify the host's challenge (register binds id = 0). // 2. Receive and verify the host's challenge (register binds id = 0).
let server_challenge = receive_verified_challenge( let server_challenge = match receive_verified_challenge(
&receiver, &receiver,
&tm, &tm,
host_public_key_bundle, host_public_key_bundle,
0, 0,
"auth_register challenge", "auth_register challenge",
) )
.await?; .await
{
Ok(c) => c,
Err(e) => {
sender.close();
return Err(e);
}
};
// 3. Sign the host's challenge over the bundle and send the proof. // 3. Sign the host's challenge over the bundle and send the proof.
let client_nonce: u128 = rand::random(); let client_nonce: u128 = rand::random();
let proof_payload = let proof_payload =
auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce); auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce);
let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?; let proof = match signed_challenge_response(keys, &proof_payload, client_nonce) {
sender.send(&proof).await?; Ok(p) => p,
Err(e) => {
sender.close();
return Err(e);
}
};
if let Err(e) = sender.send(&proof).await {
sender.close();
return Err(e);
}
// 4. Receive the host's final confirmation; extract the assigned id and // 4. Receive the host's final confirmation; extract the assigned id and
// verify the host signature binds to it. // verify the host signature binds to it.
let response = receiver.receive().await?; let response = match receiver.receive().await {
Ok(r) => r,
Err(e) => {
sender.close();
return Err(e);
}
};
let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm); let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm);
if response.get_type() != expected_type { if response.get_type() != expected_type {
sender.close();
return Err(unexpected_response_type_error( return Err(unexpected_response_type_error(
"auth_register", "auth_register",
expected_type, expected_type,
&response, &response,
)); ));
} }
check_connected(&response, &tm, "Server rejected registration")?; if let Err(e) = check_connected(&response, "Server rejected registration") {
let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) { sender.close();
return Err(e);
}
let assigned_id = match response.get_data(DataType::Id) {
DataValue::UnsignedNumber(n) => *n as u64, DataValue::UnsignedNumber(n) => *n as u64,
_ => { _ => {
sender.close();
return Err(CommunicationError::AuthenticationFailed( return Err(CommunicationError::AuthenticationFailed(
"Missing assigned ID".into(), "Missing assigned ID".into(),
)); ));
} }
}; };
verify_host_final( if let Err(e) = verify_host_final(
&response, &response,
&tm,
host_public_key_bundle, host_public_key_bundle,
assigned_id, assigned_id,
client_nonce, client_nonce,
server_challenge, server_challenge,
)?; ) {
sender.close();
return Err(e);
}
Ok(MTPConnection { Ok(MTPConnection {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
@ -470,8 +573,23 @@ mod tests {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
#[test] #[test]
fn test_auth_state_derive() { fn test_auth_state_unauthenticated_is_not_authenticated() {
assert_eq!(AuthState::Pending, AuthState::Pending); assert_ne!(AuthState::Unauthenticated, AuthState::Authenticated);
assert_ne!(AuthState::Authenticated, AuthState::Failed); assert_ne!(AuthState::Pending, AuthState::Authenticated);
}
#[cfg(feature = "crypto")]
#[test]
fn test_auth_timeout_default() {
let config = ClientConfig::new("https://localhost:4433");
assert_eq!(config.auth_timeout, Duration::from_secs(30));
}
#[cfg(feature = "crypto")]
#[test]
fn test_auth_timeout_custom() {
let config = ClientConfig::new("https://localhost:4433")
.with_auth_timeout(Duration::from_secs(10));
assert_eq!(config.auth_timeout, Duration::from_secs(10));
} }
} }

View file

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use crate::data_value::DataValue; use crate::data_value::{DataKind, DataValue};
use crate::rand_u32; use crate::rand_u32;
use mtp_common::CodecError; use mtp_common::CodecError;
use mtp_type_map::{ use mtp_type_map::{
@ -12,9 +12,7 @@ use mtp_type_map::{
}; };
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
use mtp_crypto::SigAlgorithm; use mtp_crypto::{PublicKeyBundle, SigAlgorithm, SignatureScheme};
#[cfg(feature = "crypto")]
use mtp_crypto::SignatureScheme;
const FLAG_HAS_SENDER: u8 = 0b0000_0001; const FLAG_HAS_SENDER: u8 = 0b0000_0001;
const FLAG_HAS_RECEIVER: u8 = 0b0000_0010; const FLAG_HAS_RECEIVER: u8 = 0b0000_0010;
@ -121,13 +119,147 @@ impl CommunicationValue {
self self
} }
pub fn get_data(&self, data_type: DataTypeId) -> &DataValue { pub fn get_data(&self, data_type: DataType) -> &DataValue {
self.data.get(&data_type).unwrap_or(&DataValue::Null) let tm_owned;
let tm = match &self.type_map {
Some(tm) => tm,
None => {
tm_owned = TypeMap::latest();
&tm_owned
}
};
match tm.data_id_enum(data_type) {
Some(raw_id) => self.data.get(&DataTypeId(raw_id)).unwrap_or(&DataValue::Null),
None => &DataValue::Null,
}
}
pub fn get_data_opt(&self, data_type: DataType) -> Option<&DataValue> {
let tm_owned;
let tm = match &self.type_map {
Some(tm) => tm,
None => {
tm_owned = TypeMap::latest();
&tm_owned
}
};
let raw_id = tm.data_id_enum(data_type)?;
self.data.get(&DataTypeId(raw_id))
}
pub fn has_data(&self, data_type: DataType) -> Option<DataKind> {
self.get_data_opt(data_type).map(|v| v.kind())
}
pub fn get_comm_type_enum(&self) -> Option<CommunicationType> {
let tm_owned;
let tm = match &self.type_map {
Some(tm) => tm,
None => {
tm_owned = TypeMap::latest();
&tm_owned
}
};
tm.comm_enum_id(self.comm_type.0)
} }
pub fn data(&self) -> &BTreeMap<DataTypeId, DataValue> { pub fn data(&self) -> &BTreeMap<DataTypeId, DataValue> {
&self.data &self.data
} }
pub fn data_len(&self) -> usize {
self.data.len()
}
// ── type checks ──────────────────────────────────────────────────────────
pub fn is_type(&self, comm_type: CommunicationType) -> bool {
self.get_comm_type_enum() == Some(comm_type)
}
pub fn get_type_name(&self) -> Option<&'static str> {
communication_type_name(self.comm_type.0)
}
// ── mutation ─────────────────────────────────────────────────────────────
pub fn set_data(&mut self, data_type: DataType, value: DataValue) {
let tm = self.type_map.clone().unwrap_or_else(TypeMap::latest);
if let Some(raw_id) = tm.data_id_enum(data_type) {
self.data.insert(DataTypeId(raw_id), value);
}
}
#[must_use]
pub fn with_data(mut self, data_type: DataType, value: DataValue) -> Self {
self.set_data(data_type, value);
self
}
pub fn remove_data(&mut self, data_type: DataType) -> Option<DataValue> {
let tm_owned;
let tm = match &self.type_map {
Some(tm) => tm,
None => {
tm_owned = TypeMap::latest();
&tm_owned
}
};
let raw_id = tm.data_id_enum(data_type)?;
self.data.remove(&DataTypeId(raw_id))
}
#[must_use]
pub fn reply_to(&self, comm_type: CommunicationType) -> Self {
Self::new(comm_type)
.with_sender(self.receiver)
.with_receiver(self.sender)
}
pub fn merge(&mut self, other: &CommunicationValue) {
for (id, value) in &other.data {
self.data.insert(*id, value.clone());
}
}
// ── typed iteration ──────────────────────────────────────────────────────
pub fn iter_typed_data(&self) -> impl Iterator<Item = (Option<DataType>, &DataValue)> + '_ {
let tm = self.type_map.clone().unwrap_or_else(TypeMap::latest);
self.data
.iter()
.map(move |(id, val)| (tm.data_enum_id(id.0), val))
}
// ── typed field accessors ─────────────────────────────────────────────────
pub fn get_bool(&self, data_type: DataType) -> Option<bool> {
self.get_data_opt(data_type)?.as_bool()
}
pub fn get_str(&self, data_type: DataType) -> Option<&str> {
self.get_data_opt(data_type)?.as_str()
}
pub fn get_u128(&self, data_type: DataType) -> Option<u128> {
self.get_data_opt(data_type)?.as_unsigned_number()
}
pub fn get_i128(&self, data_type: DataType) -> Option<i128> {
self.get_data_opt(data_type)?.as_signed_number()
}
pub fn get_float(&self, data_type: DataType) -> Option<(u8, u32)> {
self.get_data_opt(data_type)?.as_float()
}
pub fn get_bytes(&self, data_type: DataType) -> Option<&[u8]> {
self.get_data_opt(data_type)?.as_bytes_slice()
}
pub fn get_array(&self, data_type: DataType) -> Option<&[DataValue]> {
self.get_data_opt(data_type)?.as_array_slice()
}
} }
impl CommunicationValue { impl CommunicationValue {
@ -433,6 +565,64 @@ impl CommunicationValue {
self.frame_signature.as_ref() self.frame_signature.as_ref()
} }
/*
* Verify the frame signature using a `PublicKeyBundle`. Dispatches to
* Ed25519, ML-DSA-65, or both (DUAL) based on the stored algorithm byte.
* Returns `false` if the frame has no signature or verification fails.
*/
#[cfg(feature = "crypto")]
pub fn validate_signature(&self, pk: &PublicKeyBundle) -> bool {
let Some((alg, _)) = &self.frame_signature else {
return false;
};
struct Ed25519Verifier<'a>(&'a mtp_crypto::SignaturePublicKey);
impl SignatureScheme for Ed25519Verifier<'_> {
fn sign(&self, _: &[u8]) -> Result<Vec<u8>, mtp_crypto::CryptoError> {
Err(mtp_crypto::CryptoError::SigningFailed)
}
fn verify(&self, msg: &[u8], sig: &[u8]) -> Result<(), mtp_crypto::CryptoError> {
mtp_crypto::verify_ed25519(self.0, msg, sig)
}
}
struct MlDsaVerifier<'a>(&'a mtp_crypto::SignaturePqPublicKey);
impl SignatureScheme for MlDsaVerifier<'_> {
fn sign(&self, _: &[u8]) -> Result<Vec<u8>, mtp_crypto::CryptoError> {
Err(mtp_crypto::CryptoError::SigningFailed)
}
fn verify(&self, msg: &[u8], sig: &[u8]) -> Result<(), mtp_crypto::CryptoError> {
mtp_crypto::verify_ml_dsa(self.0, msg, sig)
}
}
match *alg {
SigAlgorithm::ED25519 => {
self.verify_frame(&Ed25519Verifier(&pk.sig_cl_public_key)).is_ok()
}
SigAlgorithm::ML_DSA_65 => {
self.verify_frame(&MlDsaVerifier(&pk.sig_pq_public_key)).is_ok()
}
SigAlgorithm::DUAL => {
// For DUAL, verify_frame passes the full combined sig to the verifier.
// We wrap a verifier that splits and checks both halves.
struct DualVerifier<'a>(&'a mtp_crypto::SignaturePublicKey, &'a mtp_crypto::SignaturePqPublicKey);
impl SignatureScheme for DualVerifier<'_> {
fn sign(&self, _: &[u8]) -> Result<Vec<u8>, mtp_crypto::CryptoError> {
Err(mtp_crypto::CryptoError::SigningFailed)
}
fn verify(&self, msg: &[u8], sig: &[u8]) -> Result<(), mtp_crypto::CryptoError> {
const ED_LEN: usize = 64;
if sig.len() < ED_LEN {
return Err(mtp_crypto::CryptoError::InvalidSignature);
}
mtp_crypto::verify_ed25519(self.0, msg, &sig[..ED_LEN])?;
mtp_crypto::verify_ml_dsa(self.1, msg, &sig[ED_LEN..])
}
}
self.verify_frame(&DualVerifier(&pk.sig_cl_public_key, &pk.sig_pq_public_key)).is_ok()
}
_ => false,
}
}
#[cfg(feature = "registry")] #[cfg(feature = "registry")]
pub fn migrate(&self, target_tm: &TypeMap) -> Result<Self, CodecError> { pub fn migrate(&self, target_tm: &TypeMap) -> Result<Self, CodecError> {
let comm_name = communication_type_name(self.comm_type.0) let comm_name = communication_type_name(self.comm_type.0)
@ -544,15 +734,23 @@ impl fmt::Display for CommunicationValue {
write!(f, ", R:{}{:X}{}", ORANGE, self.receiver, RESET)?; write!(f, ", R:{}{:X}{}", ORANGE, self.receiver, RESET)?;
} }
let name = communication_type_name(self.comm_type.0).unwrap_or("?"); let name = self
.get_comm_type_enum()
.map(|t| t.name())
.unwrap_or_else(|| communication_type_name(self.comm_type.0).unwrap_or("?"));
write!(f, ", {}: ", name)?; write!(f, ", {}: ", name)?;
let tm = self.type_map.clone().unwrap_or_else(TypeMap::latest);
write!(f, "{{")?; write!(f, "{{")?;
for (i, (key, value)) in self.data.iter().enumerate() { for (i, (raw_id, value)) in self.data.iter().enumerate() {
if i > 0 { if i > 0 {
write!(f, ", ")?; write!(f, ", ")?;
} }
let dname = data_type_name(key.0).unwrap_or("?"); let dname = tm
.data_enum_id(raw_id.0)
.map(|t| t.name())
.or_else(|| data_type_name(raw_id.0))
.unwrap_or("?");
write!(f, "{}: ", dname)?; write!(f, "{}: ", dname)?;
fmt_data_value(value, f)?; fmt_data_value(value, f)?;
} }
@ -645,11 +843,11 @@ mod tests {
assert_eq!(decoded.get_receiver(), 222); assert_eq!(decoded.get_receiver(), 222);
assert_eq!(decoded.get_type(), CommunicationType::Disconnect.to_id(&tm)); assert_eq!(decoded.get_type(), CommunicationType::Disconnect.to_id(&tm));
assert_eq!( assert_eq!(
decoded.get_data(DataType::Id.to_id(&tm)), decoded.get_data(DataType::Id),
&DataValue::Str("alice".to_string()) &DataValue::Str("alice".to_string())
); );
assert_eq!( assert_eq!(
decoded.get_data(DataType::ClientNonce.to_id(&tm)), decoded.get_data(DataType::ClientNonce),
&DataValue::SignedNumber(42) &DataValue::SignedNumber(42)
); );
} }

View file

@ -39,6 +39,28 @@ pub enum DataKind {
Null, Null,
} }
impl fmt::Display for DataKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataKind::Bool => f.write_str("Bool"),
DataKind::SignedNumber => f.write_str("SignedNumber"),
DataKind::UnsignedNumber => f.write_str("UnsignedNumber"),
DataKind::Float => f.write_str("Float"),
DataKind::Str => f.write_str("Str"),
DataKind::Bytes => f.write_str("Bytes"),
DataKind::Array(inner) => write!(f, "Array<{}>", inner),
DataKind::Container => f.write_str("Container"),
#[cfg(feature = "crypto")]
DataKind::EncryptedContainer => f.write_str("EncryptedContainer"),
#[cfg(feature = "crypto")]
DataKind::SignedContainer => f.write_str("SignedContainer"),
#[cfg(feature = "crypto")]
DataKind::SignedEncryptedContainer => f.write_str("SignedEncryptedContainer"),
DataKind::Null => f.write_str("Null"),
}
}
}
#[derive(Debug, Clone, Eq)] #[derive(Debug, Clone, Eq)]
pub enum DataValue { pub enum DataValue {
BoolTrue, BoolTrue,
@ -259,6 +281,77 @@ impl DataValue {
} }
} }
pub fn as_number(&self) -> Option<i128> {
match self {
DataValue::SignedNumber(n) => Some(*n),
DataValue::UnsignedNumber(n) => Some(*n as i128),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, DataValue::Null)
}
pub fn is_truthy(&self) -> bool {
match self {
DataValue::BoolTrue | DataValue::Bool(true) => true,
DataValue::BoolFalse | DataValue::Bool(false) | DataValue::Null => false,
DataValue::UnsignedNumber(0) | DataValue::SignedNumber(0) => false,
_ => true,
}
}
pub fn get_field(&self, key: DataTypeId) -> Option<&DataValue> {
match self {
DataValue::Container(entries) => {
entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
}
_ => None,
}
}
pub fn as_container_map(&self) -> Option<BTreeMap<DataTypeId, DataValue>> {
match self {
DataValue::Container(entries) => Some(entries.iter().cloned().collect()),
_ => None,
}
}
pub fn as_bytes_slice(&self) -> Option<&[u8]> {
match self {
DataValue::Bytes(b) => Some(b),
_ => None,
}
}
pub fn as_array_slice(&self) -> Option<&[DataValue]> {
match self {
DataValue::Array(a) => Some(a),
_ => None,
}
}
pub fn type_name(&self) -> &'static str {
match self {
DataValue::Bool(_) | DataValue::BoolTrue | DataValue::BoolFalse => "Bool",
DataValue::SignedNumber(_) => "SignedNumber",
DataValue::UnsignedNumber(_) => "UnsignedNumber",
DataValue::Float(_, _) => "Float",
DataValue::Str(_) => "Str",
DataValue::Bytes(_) => "Bytes",
DataValue::Array(_) => "Array",
DataValue::Container(_) => "Container",
#[cfg(feature = "crypto")]
DataValue::EncryptedContainer(_) => "EncryptedContainer",
#[cfg(feature = "crypto")]
DataValue::SignedContainer(_) => "SignedContainer",
#[cfg(feature = "crypto")]
DataValue::SignedEncryptedContainer(_) => "SignedEncryptedContainer",
DataValue::Null => "Null",
}
}
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub fn as_encrypted_container(&self) -> Option<Vec<u8>> { pub fn as_encrypted_container(&self) -> Option<Vec<u8>> {
match self { match self {
@ -374,6 +467,60 @@ impl DataValue {
Some(()) Some(())
} }
/*
* Verify a `SignedContainer` without mutating self. Dispatches to
* Ed25519, ML-DSA-65, or both (DUAL) based on the algorithm byte
* embedded in the blob. Returns `false` for any other variant.
*/
#[cfg(feature = "crypto")]
pub fn validate_signature(&self, pk: &PublicKeyBundle) -> bool {
let blob = match self {
DataValue::SignedContainer(b) => b,
_ => return false,
};
if blob.is_empty() {
return false;
}
let alg = blob[0];
let sig_len = match SigAlgorithm::length(alg) {
Some(n) => n,
None => return false,
};
if blob.len() < 1 + sig_len + 2 {
return false;
}
let signature = &blob[1..1 + sig_len];
let container_bytes = &blob[1 + sig_len..];
match alg {
SigAlgorithm::ED25519 => {
mtp_crypto::verify_ed25519(&pk.sig_cl_public_key, container_bytes, signature).is_ok()
}
SigAlgorithm::ML_DSA_65 => {
mtp_crypto::verify_ml_dsa(&pk.sig_pq_public_key, container_bytes, signature).is_ok()
}
SigAlgorithm::DUAL => {
const ED_LEN: usize = 64;
if signature.len() < ED_LEN {
return false;
}
let ed_ok = mtp_crypto::verify_ed25519(
&pk.sig_cl_public_key,
container_bytes,
&signature[..ED_LEN],
)
.is_ok();
let ml_ok = mtp_crypto::verify_ml_dsa(
&pk.sig_pq_public_key,
container_bytes,
&signature[ED_LEN..],
)
.is_ok();
ed_ok && ml_ok
}
_ => false,
}
}
/* /*
* Encrypt a `Container` into a `SignedEncryptedContainer` in-place. * Encrypt a `Container` into a `SignedEncryptedContainer` in-place.
* The container is first signed (with `algorithm`/`signer`), then the signed * The container is first signed (with `algorithm`/`signer`), then the signed
@ -895,6 +1042,133 @@ impl Hash for DataValue {
} }
} }
/* ================================ FROM / TRY-FROM ================================ */
impl From<bool> for DataValue {
fn from(v: bool) -> Self {
if v { DataValue::BoolTrue } else { DataValue::BoolFalse }
}
}
impl From<&str> for DataValue {
fn from(s: &str) -> Self {
DataValue::Str(s.to_string())
}
}
impl From<String> for DataValue {
fn from(s: String) -> Self {
DataValue::Str(s)
}
}
impl From<i64> for DataValue {
fn from(n: i64) -> Self {
DataValue::SignedNumber(n as i128)
}
}
impl From<i128> for DataValue {
fn from(n: i128) -> Self {
DataValue::SignedNumber(n)
}
}
impl From<u64> for DataValue {
fn from(n: u64) -> Self {
DataValue::UnsignedNumber(n as u128)
}
}
impl From<u128> for DataValue {
fn from(n: u128) -> Self {
DataValue::UnsignedNumber(n)
}
}
impl From<Vec<u8>> for DataValue {
fn from(b: Vec<u8>) -> Self {
DataValue::Bytes(b)
}
}
impl From<&[u8]> for DataValue {
fn from(b: &[u8]) -> Self {
DataValue::Bytes(b.to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataValueTypeMismatch {
pub expected: &'static str,
pub got: &'static str,
}
impl fmt::Display for DataValueTypeMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "expected {}, got {}", self.expected, self.got)
}
}
impl std::error::Error for DataValueTypeMismatch {}
impl TryFrom<DataValue> for bool {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
v.as_bool().ok_or(DataValueTypeMismatch { expected: "Bool", got: v.type_name() })
}
}
impl TryFrom<DataValue> for String {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
match v {
DataValue::Str(s) => Ok(s),
other => Err(DataValueTypeMismatch { expected: "Str", got: other.type_name() }),
}
}
}
impl TryFrom<DataValue> for i128 {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
v.as_signed_number().ok_or(DataValueTypeMismatch { expected: "SignedNumber", got: v.type_name() })
}
}
impl TryFrom<DataValue> for i64 {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
let n = v.as_signed_number().ok_or(DataValueTypeMismatch { expected: "SignedNumber", got: v.type_name() })?;
Ok(n as i64)
}
}
impl TryFrom<DataValue> for u128 {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
v.as_unsigned_number().ok_or(DataValueTypeMismatch { expected: "UnsignedNumber", got: v.type_name() })
}
}
impl TryFrom<DataValue> for u64 {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
let n = v.as_unsigned_number().ok_or(DataValueTypeMismatch { expected: "UnsignedNumber", got: v.type_name() })?;
Ok(n as u64)
}
}
impl TryFrom<DataValue> for Vec<u8> {
type Error = DataValueTypeMismatch;
fn try_from(v: DataValue) -> Result<Self, Self::Error> {
match v {
DataValue::Bytes(b) => Ok(b),
other => Err(DataValueTypeMismatch { expected: "Bytes", got: other.type_name() }),
}
}
}
/* ================================ TESTS ================================ */ /* ================================ TESTS ================================ */
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -1265,6 +1539,41 @@ mod tests {
assert!(s.contains("6:")); assert!(s.contains("6:"));
} }
#[test]
fn test_from_primitives() {
assert_eq!(DataValue::from(true), DataValue::BoolTrue);
assert_eq!(DataValue::from(false), DataValue::BoolFalse);
assert_eq!(DataValue::from("hello"), DataValue::Str("hello".to_string()));
assert_eq!(DataValue::from("hello".to_string()), DataValue::Str("hello".to_string()));
assert_eq!(DataValue::from(42i64), DataValue::SignedNumber(42));
assert_eq!(DataValue::from(42i128), DataValue::SignedNumber(42));
assert_eq!(DataValue::from(42u64), DataValue::UnsignedNumber(42));
assert_eq!(DataValue::from(42u128), DataValue::UnsignedNumber(42));
assert_eq!(DataValue::from(vec![1u8, 2, 3]), DataValue::Bytes(vec![1, 2, 3]));
assert_eq!(DataValue::from([1u8, 2, 3].as_ref()), DataValue::Bytes(vec![1, 2, 3]));
}
#[test]
fn test_try_from_ok() {
assert_eq!(bool::try_from(DataValue::BoolTrue).unwrap(), true);
assert_eq!(bool::try_from(DataValue::BoolFalse).unwrap(), false);
assert_eq!(String::try_from(DataValue::Str("hi".to_string())).unwrap(), "hi");
assert_eq!(i128::try_from(DataValue::SignedNumber(-1)).unwrap(), -1i128);
assert_eq!(i64::try_from(DataValue::SignedNumber(10)).unwrap(), 10i64);
assert_eq!(u128::try_from(DataValue::UnsignedNumber(99)).unwrap(), 99u128);
assert_eq!(u64::try_from(DataValue::UnsignedNumber(7)).unwrap(), 7u64);
assert_eq!(Vec::<u8>::try_from(DataValue::Bytes(vec![0xAB])).unwrap(), vec![0xABu8]);
}
#[test]
fn test_try_from_err() {
assert!(bool::try_from(DataValue::Null).is_err());
assert!(String::try_from(DataValue::SignedNumber(1)).is_err());
assert!(i128::try_from(DataValue::BoolTrue).is_err());
assert!(u128::try_from(DataValue::Str("x".to_string())).is_err());
assert!(Vec::<u8>::try_from(DataValue::Null).is_err());
}
#[test] #[test]
fn test_array_display() { fn test_array_display() {
let dv = DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]); let dv = DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]);

View file

@ -16,6 +16,8 @@ pub enum CodecError {
TooManyEntries, TooManyEntries,
#[error("Crypto failed: {0}")] #[error("Crypto failed: {0}")]
CryptoFailed(String), CryptoFailed(String),
#[error("Missing required field: {0}")]
MissingField(String),
} }
/* ================================ TESTS ================================ */ /* ================================ TESTS ================================ */

View file

@ -21,6 +21,12 @@ impl From<Vec<u8>> for EncryptionPrivateKey {
} }
} }
impl From<&[u8]> for EncryptionPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)] #[derive(Zeroize, ZeroizeOnDrop)]
@ -42,6 +48,12 @@ impl From<Vec<u8>> for SignaturePrivateKey {
} }
} }
impl From<&[u8]> for SignaturePrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)] #[derive(Clone)]
@ -63,6 +75,12 @@ impl From<Vec<u8>> for EncryptionPublicKey {
} }
} }
impl From<&[u8]> for EncryptionPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)] #[derive(Clone)]
@ -84,6 +102,12 @@ impl From<Vec<u8>> for SignaturePublicKey {
} }
} }
impl From<&[u8]> for SignaturePublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)] #[derive(Zeroize, ZeroizeOnDrop)]
@ -105,6 +129,12 @@ impl From<Vec<u8>> for KemPrivateKey {
} }
} }
impl From<&[u8]> for KemPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)] #[derive(Clone)]
@ -126,6 +156,12 @@ impl From<Vec<u8>> for KemPublicKey {
} }
} }
impl From<&[u8]> for KemPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)] #[derive(Clone)]
@ -147,6 +183,12 @@ impl From<Vec<u8>> for SignaturePqPublicKey {
} }
} }
impl From<&[u8]> for SignaturePqPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)] #[derive(Zeroize, ZeroizeOnDrop)]
@ -168,6 +210,12 @@ impl From<Vec<u8>> for SignaturePqPrivateKey {
} }
} }
impl From<&[u8]> for SignaturePqPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(ZeroizeOnDrop)] #[derive(ZeroizeOnDrop)]
pub struct Keyring { pub struct Keyring {

View file

@ -31,13 +31,13 @@ pub fn process_and_respond(
let sig_id = DataTypeId(tm.data_id_enum(DataType::SignedPayload).unwrap()); let sig_id = DataTypeId(tm.data_id_enum(DataType::SignedPayload).unwrap());
let secure_id = DataTypeId(tm.data_id_enum(DataType::SecurePayload).unwrap()); let secure_id = DataTypeId(tm.data_id_enum(DataType::SecurePayload).unwrap());
let description = msg.get_data(desc_id); let description = msg.get_data(DataType::Description);
let timestamp = msg.get_data(ts_id); let timestamp = msg.get_data(DataType::Timestamp);
let data = msg.get_data(data_id); let data = msg.get_data(DataType::Data);
let flags = msg.get_data(flags_id); let flags = msg.get_data(DataType::Flags);
let value = msg.get_data(value_id); let value = msg.get_data(DataType::Value);
let binary = msg.get_data(bin_id); let binary = msg.get_data(DataType::BinaryData);
let items = msg.get_data(items_id); let items = msg.get_data(DataType::Items);
println!( println!(
" Description: {}", " Description: {}",
@ -54,7 +54,7 @@ pub fn process_and_respond(
let mut sig_status = String::from("SignedPayload: not present"); let mut sig_status = String::from("SignedPayload: not present");
let mut secure_status = String::from("SecurePayload: not present"); let mut secure_status = String::from("SecurePayload: not present");
let enc = msg.get_data(enc_id); let enc = msg.get_data(DataType::EncryptedPayload);
if matches!(enc, DataValue::EncryptedContainer(_)) { if matches!(enc, DataValue::EncryptedContainer(_)) {
let mut dv = enc.clone(); let mut dv = enc.clone();
if dv.decrypt_into_container(host_keyring, b"demo-aad").is_some() { if dv.decrypt_into_container(host_keyring, b"demo-aad").is_some() {
@ -68,7 +68,7 @@ pub fn process_and_respond(
} }
} }
let sig = msg.get_data(sig_id); let sig = msg.get_data(DataType::SignedPayload);
if matches!(sig, DataValue::SignedContainer(_)) { if matches!(sig, DataValue::SignedContainer(_)) {
if let Some(pk_bundle) = client_pk { if let Some(pk_bundle) = client_pk {
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone()); let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
@ -88,7 +88,7 @@ pub fn process_and_respond(
} }
} }
let secure = msg.get_data(secure_id); let secure = msg.get_data(DataType::SecurePayload);
if matches!(secure, DataValue::SignedEncryptedContainer(_)) { if matches!(secure, DataValue::SignedEncryptedContainer(_)) {
if let Some(pk_bundle) = client_pk { if let Some(pk_bundle) = client_pk {
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone()); let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());

View file

@ -9,6 +9,7 @@ mtp-codec = { path = "../codec", features = ["registry"] }
mtp-transport = { path = "../transport", features = ["host"] } mtp-transport = { path = "../transport", features = ["host"] }
mtp-crypto = { path = "../crypto", optional = true } mtp-crypto = { path = "../crypto", optional = true }
rand = "0.8" rand = "0.8"
tokio = { version = "1", features = ["time"] }
[features] [features]
crypto = ["dep:mtp-crypto", "mtp-codec/crypto"] crypto = ["dep:mtp-crypto", "mtp-codec/crypto"]

View file

@ -5,7 +5,24 @@ use mtp_codec::{
use mtp_common::CommunicationError; use mtp_common::CommunicationError;
use mtp_transport::{Policy, Receiver, Sender}; use mtp_transport::{Policy, Receiver, Sender};
use std::net::IpAddr; use std::net::IpAddr;
use std::pin::Pin;
use std::{error::Error, fmt}; use std::{error::Error, fmt};
use tokio::time::Duration;
/* ---- async callback type aliases ---- */
#[cfg(feature = "crypto")]
type GetExistingUser = Box<
dyn Fn(u64) -> Pin<Box<dyn std::future::Future<Output = Option<mtp_crypto::PublicKeyBundle>> + Send>>
+ Send
+ Sync,
>;
#[cfg(feature = "crypto")]
type CompleteRegister = Box<
dyn Fn(mtp_crypto::PublicKeyBundle) -> Pin<Box<dyn std::future::Future<Output = u64> + Send>>
+ Send
+ Sync,
>;
/* Host configuration. */ /* Host configuration. */
pub struct HostConfig { pub struct HostConfig {
@ -17,11 +34,13 @@ pub struct HostConfig {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub require_authentication: bool, pub require_authentication: bool,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_timeout: Duration,
#[cfg(feature = "crypto")]
pub host_keyring: mtp_crypto::Keyring, pub host_keyring: mtp_crypto::Keyring,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub get_existing_user: Box<dyn Fn(u64) -> Option<mtp_crypto::PublicKeyBundle> + Send + Sync>, pub get_existing_user: GetExistingUser,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub complete_register: Box<dyn Fn(mtp_crypto::PublicKeyBundle) -> u64 + Send + Sync>, pub complete_register: CompleteRegister,
} }
impl HostConfig { impl HostConfig {
@ -34,6 +53,8 @@ impl HostConfig {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
require_authentication: false, require_authentication: false,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_timeout: Duration::from_secs(30),
#[cfg(feature = "crypto")]
host_keyring: mtp_crypto::Keyring::new( host_keyring: mtp_crypto::Keyring::new(
mtp_crypto::KemPublicKey::new(Vec::new()), mtp_crypto::KemPublicKey::new(Vec::new()),
mtp_crypto::KemPrivateKey::new(Vec::new()), mtp_crypto::KemPrivateKey::new(Vec::new()),
@ -43,9 +64,9 @@ impl HostConfig {
mtp_crypto::SignaturePrivateKey::new(Vec::new()), mtp_crypto::SignaturePrivateKey::new(Vec::new()),
), ),
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
get_existing_user: Box::new(|_| None), get_existing_user: Box::new(|_| Box::pin(async { None })),
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
complete_register: Box::new(|_| 0), complete_register: Box::new(|_| Box::pin(async { 0 })),
} }
} }
@ -53,8 +74,14 @@ impl HostConfig {
pub fn with_authentication( pub fn with_authentication(
mut self, mut self,
host_keyring: mtp_crypto::Keyring, host_keyring: mtp_crypto::Keyring,
get_existing_user: impl Fn(u64) -> Option<mtp_crypto::PublicKeyBundle> + Send + Sync + 'static, get_existing_user: impl Fn(u64) -> Pin<Box<dyn std::future::Future<Output = Option<mtp_crypto::PublicKeyBundle>> + Send>>
complete_register: impl Fn(mtp_crypto::PublicKeyBundle) -> u64 + Send + Sync + 'static, + Send
+ Sync
+ 'static,
complete_register: impl Fn(mtp_crypto::PublicKeyBundle) -> Pin<Box<dyn std::future::Future<Output = u64> + Send>>
+ Send
+ Sync
+ 'static,
) -> Self { ) -> Self {
self.require_authentication = true; self.require_authentication = true;
self.host_keyring = host_keyring; self.host_keyring = host_keyring;
@ -62,6 +89,12 @@ impl HostConfig {
self.complete_register = Box::new(complete_register); self.complete_register = Box::new(complete_register);
self self
} }
#[cfg(feature = "crypto")]
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
self.auth_timeout = timeout;
self
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -70,6 +103,7 @@ pub enum AcceptError {
MissingVersion, MissingVersion,
UnsupportedVersion(Version), UnsupportedVersion(Version),
AuthenticationFailed(String), AuthenticationFailed(String),
AuthenticationTimedOut,
Send(CommunicationError), Send(CommunicationError),
} }
@ -85,6 +119,7 @@ impl fmt::Display for AcceptError {
write!(f, "unsupported protocol version: {version}") write!(f, "unsupported protocol version: {version}")
} }
Self::AuthenticationFailed(reason) => write!(f, "authentication failed: {reason}"), Self::AuthenticationFailed(reason) => write!(f, "authentication failed: {reason}"),
Self::AuthenticationTimedOut => write!(f, "authentication handshake timed out"),
Self::Send(error) => write!(f, "failed to send handshake message: {error}"), Self::Send(error) => write!(f, "failed to send handshake message: {error}"),
} }
} }
@ -95,6 +130,7 @@ impl Error for AcceptError {}
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthState { pub enum AuthState {
Unauthenticated,
Pending, Pending,
Authenticated, Authenticated,
Failed, Failed,
@ -158,7 +194,16 @@ impl MTPHost {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
if self.config.require_authentication { if self.config.require_authentication {
return self.accept_authenticated(sender, receiver).await; let timeout = self.config.auth_timeout;
return match tokio::time::timeout(
timeout,
self.accept_authenticated(sender, receiver),
)
.await
{
Ok(result) => result,
Err(_) => Err(AcceptError::AuthenticationTimedOut),
};
} }
// Read the first message (always encoded with reserved types). // Read the first message (always encoded with reserved types).
@ -167,13 +212,6 @@ impl MTPHost {
Err(e) => return Err(AcceptError::Receive(e)), Err(e) => return Err(AcceptError::Receive(e)),
}; };
/*
* Extract the client's version from the first message.
* The client is expected to send DataType::Version (reserved ID 3)
* as a DataValue::Str("X.Y").
*
* Then negotiate the version for single-version clients
*/
let client_version = match extract_version(&first_msg) { let client_version = match extract_version(&first_msg) {
Some(v) => v, Some(v) => v,
None => return Err(AcceptError::MissingVersion), None => return Err(AcceptError::MissingVersion),
@ -195,7 +233,7 @@ impl MTPHost {
sender, sender,
receiver, receiver,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_state: AuthState::Authenticated, auth_state: AuthState::Unauthenticated,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
client_id: 0, client_id: 0,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -280,20 +318,31 @@ impl MTPHost {
}; };
// ===== Step 1: receive the client's unsigned hello ===== // ===== Step 1: receive the client's unsigned hello =====
let hello = receiver.receive().await.map_err(AcceptError::Receive)?; let hello = match receiver.receive().await {
let version_str = match hello.get_data(DataType::Version.to_id(&tm)) { Ok(m) => m,
Err(e) => {
sender.close();
return Err(AcceptError::Receive(e));
}
};
let version_str = match hello.get_data(DataType::Version) {
DataValue::Str(s) => s.clone(), DataValue::Str(s) => s.clone(),
_ => { _ => {
sender.close(); sender.close();
return Err(AcceptError::MissingVersion); return Err(AcceptError::MissingVersion);
} }
}; };
let client_version = Version::parse(&version_str).ok_or(AcceptError::MissingVersion)?; let client_version = match Version::parse(&version_str) {
Some(v) => v,
None => {
sender.close();
return Err(AcceptError::MissingVersion);
}
};
let (flow, response_type) = let (flow, response_type) =
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) { if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
// LOGIN: look up the claimed user before issuing a challenge. let cid = match hello.get_data(DataType::Id) {
let cid = match hello.get_data(DataType::Id.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n as u64, DataValue::UnsignedNumber(n) => *n as u64,
_ => { _ => {
sender.close(); sender.close();
@ -302,7 +351,7 @@ impl MTPHost {
)); ));
} }
}; };
let bundle = match (self.config.get_existing_user)(cid) { let bundle = match (self.config.get_existing_user)(cid).await {
Some(b) => b, Some(b) => b,
None => { None => {
let rejection = CommunicationValue::new( let rejection = CommunicationValue::new(
@ -321,8 +370,7 @@ impl MTPHost {
mtp_codec::CommunicationType::IdentificationResponse, mtp_codec::CommunicationType::IdentificationResponse,
) )
} else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) { } else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
// REGISTER: the client presents the bundle it wants to register. let bundle = match hello.get_data(DataType::PublicKeys) {
let bundle = match hello.get_data(DataType::PublicKeys.to_id(&tm)) {
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| { DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| {
AcceptError::AuthenticationFailed("invalid public key bundle".into()) AcceptError::AuthenticationFailed("invalid public key bundle".into())
})?, })?,
@ -345,7 +393,6 @@ impl MTPHost {
)); ));
}; };
// The id bound into the challenge (0 for register: none assigned yet).
let challenge_id = match &flow { let challenge_id = match &flow {
Flow::Login { id, .. } => *id, Flow::Login { id, .. } => *id,
Flow::Register { .. } => 0, Flow::Register { .. } => 0,
@ -366,20 +413,26 @@ impl MTPHost {
challenge_msg = challenge_msg challenge_msg = challenge_msg
.add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig)); .add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig));
} }
sender if let Err(e) = sender.send(&challenge_msg).await {
.send(&challenge_msg) sender.close();
.await return Err(AcceptError::Send(e));
.map_err(AcceptError::Send)?; }
// ===== Step 3: receive and verify the client's proof ===== // ===== Step 3: receive and verify the client's proof =====
let proof = receiver.receive().await.map_err(AcceptError::Receive)?; let proof = match receiver.receive().await {
Ok(m) => m,
Err(e) => {
sender.close();
return Err(AcceptError::Receive(e));
}
};
if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) { if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) {
sender.close(); sender.close();
return Err(AcceptError::AuthenticationFailed( return Err(AcceptError::AuthenticationFailed(
"missing challenge response".into(), "missing challenge response".into(),
)); ));
} }
let client_nonce = match proof.get_data(DataType::ClientNonce.to_id(&tm)) { let client_nonce = match proof.get_data(DataType::ClientNonce) {
DataValue::UnsignedNumber(n) => *n, DataValue::UnsignedNumber(n) => *n,
_ => { _ => {
sender.close(); sender.close();
@ -388,7 +441,7 @@ impl MTPHost {
)); ));
} }
}; };
let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) { let sig_bytes = match proof.get_data(DataType::Signature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => { _ => {
sender.close(); sender.close();
@ -397,7 +450,7 @@ impl MTPHost {
)); ));
} }
}; };
let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature.to_id(&tm)) { let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
@ -439,7 +492,7 @@ impl MTPHost {
let (assigned_id, client_bundle) = match flow { let (assigned_id, client_bundle) = match flow {
Flow::Login { id, bundle } => (id, bundle), Flow::Login { id, bundle } => (id, bundle),
Flow::Register { bundle, .. } => { Flow::Register { bundle, .. } => {
let new_id = (self.config.complete_register)(bundle.clone()); let new_id = (self.config.complete_register)(bundle.clone()).await;
(new_id, bundle) (new_id, bundle)
} }
}; };
@ -464,14 +517,26 @@ impl MTPHost {
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig)); response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
} }
sender.send(&response).await.map_err(AcceptError::Send)?; if let Err(e) = sender.send(&response).await {
sender.finish_stream().await.map_err(AcceptError::Send)?; sender.close();
return Err(AcceptError::Send(e));
}
if let Err(e) = sender.finish_stream().await {
sender.close();
return Err(AcceptError::Send(e));
}
// ===== Version negotiation ===== // ===== Version negotiation =====
let negotiated = self let negotiated = match self
.registry .registry
.negotiate(std::slice::from_ref(&client_version)) .negotiate(std::slice::from_ref(&client_version))
.ok_or(AcceptError::UnsupportedVersion(client_version))?; {
Some(v) => v,
None => {
sender.close();
return Err(AcceptError::UnsupportedVersion(client_version));
}
};
let codec = VersionedCodec::new(self.registry.clone()); let codec = VersionedCodec::new(self.registry.clone());
Ok(Some(MTPConnection { Ok(Some(MTPConnection {
@ -493,8 +558,7 @@ impl MTPHost {
* (reserved ID 3) mapping to `DataValue::Str("major.minor")`. * (reserved ID 3) mapping to `DataValue::Str("major.minor")`.
*/ */
fn extract_version(msg: &CommunicationValue) -> Option<Version> { fn extract_version(msg: &CommunicationValue) -> Option<Version> {
let tm = TypeMap::latest(); let value = msg.get_data(DataType::Version);
let value = msg.get_data(DataType::Version.to_id(&tm));
match value { match value {
DataValue::Str(s) => Version::parse(s.as_str()), DataValue::Str(s) => Version::parse(s.as_str()),
_ => None, _ => None,
@ -541,4 +605,11 @@ mod tests {
.add_data(DataType::Version.to_id(&tm), DataValue::UnsignedNumber(42)); .add_data(DataType::Version.to_id(&tm), DataValue::UnsignedNumber(42));
assert!(extract_version(&msg).is_none()); assert!(extract_version(&msg).is_none());
} }
#[cfg(feature = "crypto")]
#[test]
fn auth_state_unauthenticated_is_not_authenticated() {
assert_ne!(AuthState::Unauthenticated, AuthState::Authenticated);
assert_ne!(AuthState::Pending, AuthState::Authenticated);
}
} }

View file

@ -55,7 +55,7 @@ fn assert_numbered_message(
) { ) {
assert_eq!(message.get_type(), comm_type.to_id(tm)); assert_eq!(message.get_type(), comm_type.to_id(tm));
assert_eq!( assert_eq!(
message.get_data(DataType::PqSignature.to_id(tm)).clone(), message.get_data(DataType::PqSignature).clone(),
DataValue::UnsignedNumber(value) DataValue::UnsignedNumber(value)
); );
} }

View file

@ -261,12 +261,15 @@ fn generate(config: &Config, multi_version: bool) -> String {
generate_versioned_constructors(&mut out, &sorted); generate_versioned_constructors(&mut out, &sorted);
generate_builtin_type_maps(&mut out, &sorted); generate_builtin_type_maps(&mut out, &sorted);
generate_lookup_methods(&mut out, config, &sorted); generate_lookup_methods(&mut out, config, &sorted);
generate_all_types_methods(&mut out, config, &sorted);
} else { } else {
generate_single_version_lookup(&mut out, config); generate_single_version_lookup(&mut out, config);
generate_all_types_methods_single(&mut out, config);
} }
generate_enum_conversion_methods(&mut out); generate_enum_conversion_methods(&mut out);
generate_reverse_lookups(&mut out, config, &sorted, multi_version); generate_reverse_lookups(&mut out, config, &sorted, multi_version);
generate_id_display_impls(&mut out);
out out
} }
@ -564,6 +567,78 @@ fn generate_lookup_methods(
writeln!(out, " _ => None,").unwrap(); writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap(); writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap(); writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
// Reverse lookups: wire ID → enum variant (version-aware)
writeln!(
out,
" pub fn comm_enum_id(&self, id: u16) -> Option<CommunicationType> {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
for (version_key, major, minor) in sorted_versions {
let tm_cfg = &config.type_maps[version_key];
writeln!(
out,
" Version({}, {}) => match id {{",
major, minor
)
.unwrap();
for entry in RESERVED_COMM_TYPES {
writeln!(
out,
" {} => Some(CommunicationType::{}),",
entry.id, entry.name
)
.unwrap();
}
for (name, id) in &tm_cfg.communication_types {
writeln!(
out,
" {} => Some(CommunicationType::{}),",
id, name
)
.unwrap();
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }},").unwrap();
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(
out,
" pub fn data_enum_id(&self, id: u16) -> Option<DataType> {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
for (version_key, major, minor) in sorted_versions {
let tm_cfg = &config.type_maps[version_key];
writeln!(
out,
" Version({}, {}) => match id {{",
major, minor
)
.unwrap();
for entry in RESERVED_DATA_TYPES {
writeln!(
out,
" {} => Some(DataType::{}),",
entry.id, entry.name
)
.unwrap();
}
for (name, id) in &tm_cfg.data_types {
writeln!(out, " {} => Some(DataType::{}),", id, name).unwrap();
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }},").unwrap();
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap(); writeln!(out, "}}").unwrap();
writeln!(out).unwrap(); writeln!(out).unwrap();
} }
@ -631,6 +706,66 @@ fn generate_single_version_lookup(out: &mut String, config: &Config) {
writeln!(out, " _ => None,").unwrap(); writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap(); writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap(); writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
// Reverse lookups: wire ID → enum variant (single version)
writeln!(
out,
" pub fn comm_enum_id(&self, id: u16) -> Option<CommunicationType> {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
writeln!(out, " PROTOCOL_VERSION => match id {{").unwrap();
for entry in RESERVED_COMM_TYPES {
writeln!(
out,
" {} => Some(CommunicationType::{}),",
entry.id, entry.name
)
.unwrap();
}
if let Some(tm_cfg) = tm_cfg {
for (name, id) in &tm_cfg.communication_types {
writeln!(
out,
" {} => Some(CommunicationType::{}),",
id, name
)
.unwrap();
}
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }},").unwrap();
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(
out,
" pub fn data_enum_id(&self, id: u16) -> Option<DataType> {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
writeln!(out, " PROTOCOL_VERSION => match id {{").unwrap();
for entry in RESERVED_DATA_TYPES {
writeln!(
out,
" {} => Some(DataType::{}),",
entry.id, entry.name
)
.unwrap();
}
if let Some(tm_cfg) = tm_cfg {
for (name, id) in &tm_cfg.data_types {
writeln!(out, " {} => Some(DataType::{}),", id, name).unwrap();
}
}
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }},").unwrap();
writeln!(out, " _ => None,").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap(); writeln!(out, "}}").unwrap();
writeln!(out).unwrap(); writeln!(out).unwrap();
} }
@ -745,3 +880,187 @@ fn generate_enum_conversion_methods(out: &mut String) {
writeln!(out, "}}").unwrap(); writeln!(out, "}}").unwrap();
writeln!(out).unwrap(); writeln!(out).unwrap();
} }
fn generate_all_types_methods(
out: &mut String,
config: &Config,
sorted_versions: &[(String, u16, u16)],
) {
writeln!(out, "#[allow(unreachable_patterns)]").unwrap();
writeln!(out, "impl TypeMap {{").unwrap();
writeln!(
out,
" pub fn all_comm_types(&self) -> &'static [CommunicationType] {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
for (version_key, major, minor) in sorted_versions {
let tm_cfg = &config.type_maps[version_key];
write!(out, " Version({}, {}) => &[", major, minor).unwrap();
let mut first = true;
for entry in RESERVED_COMM_TYPES {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "CommunicationType::{}", entry.name).unwrap();
first = false;
}
for (name, _) in &tm_cfg.communication_types {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "CommunicationType::{}", name).unwrap();
first = false;
}
writeln!(out, "],").unwrap();
}
writeln!(out, " _ => &[],").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(
out,
" pub fn all_data_types(&self) -> &'static [DataType] {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
for (version_key, major, minor) in sorted_versions {
let tm_cfg = &config.type_maps[version_key];
write!(out, " Version({}, {}) => &[", major, minor).unwrap();
let mut first = true;
for entry in RESERVED_DATA_TYPES {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "DataType::{}", entry.name).unwrap();
first = false;
}
for (name, _) in &tm_cfg.data_types {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "DataType::{}", name).unwrap();
first = false;
}
writeln!(out, "],").unwrap();
}
writeln!(out, " _ => &[],").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
writeln!(out).unwrap();
}
fn generate_all_types_methods_single(out: &mut String, config: &Config) {
let tm_cfg = config.type_maps.get(&config.protocol_version);
writeln!(out, "#[allow(unreachable_patterns)]").unwrap();
writeln!(out, "impl TypeMap {{").unwrap();
writeln!(
out,
" pub fn all_comm_types(&self) -> &'static [CommunicationType] {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
write!(out, " PROTOCOL_VERSION => &[").unwrap();
let mut first = true;
for entry in RESERVED_COMM_TYPES {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "CommunicationType::{}", entry.name).unwrap();
first = false;
}
if let Some(tm_cfg) = tm_cfg {
for (name, _) in &tm_cfg.communication_types {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "CommunicationType::{}", name).unwrap();
first = false;
}
}
writeln!(out, "],").unwrap();
writeln!(out, " _ => &[],").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(
out,
" pub fn all_data_types(&self) -> &'static [DataType] {{"
)
.unwrap();
writeln!(out, " match self.version {{").unwrap();
write!(out, " PROTOCOL_VERSION => &[").unwrap();
let mut first = true;
for entry in RESERVED_DATA_TYPES {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "DataType::{}", entry.name).unwrap();
first = false;
}
if let Some(tm_cfg) = tm_cfg {
for (name, _) in &tm_cfg.data_types {
if !first {
write!(out, ", ").unwrap();
}
write!(out, "DataType::{}", name).unwrap();
first = false;
}
}
writeln!(out, "],").unwrap();
writeln!(out, " _ => &[],").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
writeln!(out).unwrap();
}
fn generate_id_display_impls(out: &mut String) {
writeln!(out, "impl std::fmt::Display for CommunicationTypeId {{").unwrap();
writeln!(
out,
" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
)
.unwrap();
writeln!(
out,
" match communication_type_name(self.0) {{"
)
.unwrap();
writeln!(out, " Some(name) => f.write_str(name),").unwrap();
writeln!(
out,
" None => write!(f, \"CommTypeId({{}})\", self.0),"
)
.unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
writeln!(out).unwrap();
writeln!(out, "impl std::fmt::Display for DataTypeId {{").unwrap();
writeln!(
out,
" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
)
.unwrap();
writeln!(
out,
" match data_type_name(self.0) {{"
)
.unwrap();
writeln!(out, " Some(name) => f.write_str(name),").unwrap();
writeln!(
out,
" None => write!(f, \"DataTypeId({{}})\", self.0),"
)
.unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
writeln!(out).unwrap();
}

View file

@ -62,6 +62,16 @@ impl std::fmt::Display for Version {
} }
} }
impl Version {
pub fn is_newer_than(&self, other: &Version) -> bool {
self > other
}
pub fn is_compatible_with(&self, other: &Version) -> bool {
self.0 == other.0
}
}
/* /*
* A single protocol version's type dictionary. * A single protocol version's type dictionary.
* Compiled into the client, or loaded by the host via the registry. * Compiled into the client, or loaded by the host via the registry.

View file

@ -159,11 +159,11 @@ fn verify_host_challenge(
id: u64, id: u64,
server_challenge: u128, server_challenge: u128,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
let sig = match challenge.get_data(DataType::Signature.to_id(tm)) { let sig = match challenge.get_data(DataType::Signature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => return Err(js_error("missing host challenge signature")), _ => return Err(js_error("missing host challenge signature")),
}; };
let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) { let pq_sig = match challenge.get_data(DataType::PqSignature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
@ -191,14 +191,14 @@ fn verify_host_final(
client_nonce: u128, client_nonce: u128,
server_challenge: u128, server_challenge: u128,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
if *resp.get_data(DataType::ClientNonce.to_id(tm)) != DataValue::UnsignedNumber(client_nonce) { if *resp.get_data(DataType::ClientNonce) != DataValue::UnsignedNumber(client_nonce) {
return Err(js_error("nonce mismatch")); return Err(js_error("nonce mismatch"));
} }
let host_sig = match resp.get_data(DataType::Signature.to_id(tm)) { let host_sig = match resp.get_data(DataType::Signature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => return Err(js_error("missing host signature")), _ => return Err(js_error("missing host signature")),
}; };
let host_pq_sig = match resp.get_data(DataType::PqSignature.to_id(tm)) { let host_pq_sig = match resp.get_data(DataType::PqSignature) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
@ -393,7 +393,7 @@ impl WasmClient {
)); ));
} }
if resp_comm.get_data(DataType::Connected.to_id(&tm)) != &DataValue::BoolTrue { if resp_comm.get_data(DataType::Connected) != &DataValue::BoolTrue {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
return Err(js_error("host rejected authentication")); return Err(js_error("host rejected authentication"));
} }
@ -412,7 +412,7 @@ impl WasmClient {
} }
// Extract assigned ID // Extract assigned ID
let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) { let assigned_id = match resp_comm.get_data(DataType::Id) {
DataValue::UnsignedNumber(n) => *n as u64, DataValue::UnsignedNumber(n) => *n as u64,
_ => { _ => {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
@ -495,12 +495,12 @@ impl WasmClient {
)); ));
} }
if resp_comm.get_data(DataType::Connected.to_id(&tm)) != &DataValue::BoolTrue { if resp_comm.get_data(DataType::Connected) != &DataValue::BoolTrue {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
return Err(js_error("host rejected registration")); return Err(js_error("host rejected registration"));
} }
let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) { let assigned_id = match resp_comm.get_data(DataType::Id) {
DataValue::UnsignedNumber(n) => *n as u64, DataValue::UnsignedNumber(n) => *n as u64,
_ => { _ => {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
@ -731,7 +731,7 @@ impl WasmClient {
)); ));
} }
let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(tm)) { let server_challenge = match challenge.get_data(DataType::ServerNonce) {
DataValue::UnsignedNumber(n) => *n, DataValue::UnsignedNumber(n) => *n,
_ => { _ => {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);

View file

@ -254,26 +254,26 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
.map_err(|e| js_error(&format!("parse failed: {}", e)))?; .map_err(|e| js_error(&format!("parse failed: {}", e)))?;
let connected = matches!( let connected = matches!(
comm.get_data(DataType::Connected.to_id(&TypeMap::latest())), comm.get_data(DataType::Connected)),
DataValue::BoolTrue DataValue::BoolTrue
); );
let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) { let client_nonce = match comm.get_data(DataType::ClientNonce)) {
DataValue::UnsignedNumber(n) => Some(*n), DataValue::UnsignedNumber(n) => Some(*n),
_ => None, _ => None,
}; };
let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) { let assigned_id = match comm.get_data(DataType::Id)) {
DataValue::UnsignedNumber(n) => Some(*n as u64), DataValue::UnsignedNumber(n) => Some(*n as u64),
_ => None, _ => None,
}; };
let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) { let timestamp = match comm.get_data(DataType::Timestamp)) {
DataValue::UnsignedNumber(n) => Some(*n), DataValue::UnsignedNumber(n) => Some(*n),
_ => None, _ => None,
}; };
let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) { let signature = match comm.get_data(DataType::Signature)) {
DataValue::Bytes(b) => Some(b.clone()), DataValue::Bytes(b) => Some(b.clone()),
_ => None, _ => None,
}; };
@ -376,11 +376,11 @@ mod tests {
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm)); assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 42); assert_eq!(cv.get_sender(), 42);
assert_eq!( assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)), cv.get_data(DataType::Description),
&DataValue::Str("test-ping".into()) &DataValue::Str("test-ping".into())
); );
assert_eq!( assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)), cv.get_data(DataType::Timestamp),
&DataValue::UnsignedNumber(1234567890) &DataValue::UnsignedNumber(1234567890)
); );
} }
@ -395,15 +395,15 @@ mod tests {
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm)); assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 99); assert_eq!(cv.get_sender(), 99);
assert_eq!( assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)), cv.get_data(DataType::Description),
&DataValue::Str("with-data".into()) &DataValue::Str("with-data".into())
); );
assert_eq!( assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)), cv.get_data(DataType::Timestamp),
&DataValue::UnsignedNumber(555) &DataValue::UnsignedNumber(555)
); );
assert_eq!( assert_eq!(
cv.get_data(DataType::Id.to_id(&tm)), cv.get_data(DataType::Id),
&DataValue::Bytes(payload.to_vec()) &DataValue::Bytes(payload.to_vec())
); );
} }