Something went wrong. Try again.
[AWIP] Local-first peer-to-peer connectivity and sharing across Android, Linux and Windows, made with Tauri
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534#![allow(dead_code)]
use std::collections::HashSet;use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use super::limits::ProtocolLimits;
const HEX_256_LENGTH: usize = 64;const SAS_CODE_LENGTH: usize = 6;
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingStart { pub pairing_id: String, pub device_id: String, pub certificate_fingerprint: String, pub protocol_versions: Vec<u16>, pub nonce_commitment: String,}
impl PairingStart { #[allow(clippy::too_many_arguments)] pub fn new( pairing_id: impl Into<String>, device_id: impl Into<String>, certificate_fingerprint: impl Into<String>, protocol_versions: Vec<u16>, nonce_commitment: impl Into<String>, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), device_id: device_id.into(), certificate_fingerprint: certificate_fingerprint.into(), protocol_versions, nonce_commitment: nonce_commitment.into(), }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_identifier("device_id", &self.device_id)?; validate_hex_256("certificate_fingerprint", &self.certificate_fingerprint)?; validate_protocol_versions(&self.protocol_versions)?; validate_hex_256("nonce_commitment", &self.nonce_commitment) }}
/// Responder's public nonce commitment after receiving `pair.start`#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingCommit { pub pairing_id: String, pub nonce_commitment: String, #[serde(default)] pub certificate_fingerprint: Option<String>,}
impl PairingCommit { pub fn new( pairing_id: impl Into<String>, nonce_commitment: impl Into<String>, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), nonce_commitment: nonce_commitment.into(), certificate_fingerprint: None, }; message.validate()?; Ok(message) }
pub fn new_with_certificate( pairing_id: impl Into<String>, nonce_commitment: impl Into<String>, certificate_fingerprint: impl Into<String>, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), nonce_commitment: nonce_commitment.into(), certificate_fingerprint: Some(certificate_fingerprint.into()), }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_hex_256("nonce_commitment", &self.nonce_commitment)?; if let Some(fingerprint) = &self.certificate_fingerprint { if fingerprint.len() != HEX_256_LENGTH { return Err(eros::error!(PairingError::InvalidHexLength { field: "certificate_fingerprint", length: fingerprint.len(), expected: HEX_256_LENGTH, })); } if !fingerprint .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) { return Err(eros::error!(PairingError::InvalidHexValue { field: "certificate_fingerprint" })); } } Ok(()) }}
/// Responder's temporary nonce reveal.#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingNonce { pub pairing_id: String, pub nonce: String,}
impl PairingNonce { pub fn new(pairing_id: impl Into<String>, nonce: impl Into<String>) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), nonce: nonce.into(), }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_hex_256("nonce", &self.nonce) }}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingReveal { pub pairing_id: String, pub nonce: String,}
impl PairingReveal { pub fn new(pairing_id: impl Into<String>, nonce: impl Into<String>) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), nonce: nonce.into(), }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_hex_256("nonce", &self.nonce) }}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingCode { pub pairing_id: String, pub sas_code: String, pub expires_at_ms: u64,}
impl PairingCode { pub fn new( pairing_id: impl Into<String>, sas_code: impl Into<String>, expires_at_ms: u64, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), sas_code: sas_code.into(), expires_at_ms, }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_sas_code(&self.sas_code)?; validate_expiry(self.expires_at_ms) }}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingConfirm { pub pairing_id: String, pub sas_code: Option<String>, pub confirmed: bool,}
impl PairingConfirm { pub fn accepted( pairing_id: impl Into<String>, sas_code: impl Into<String>, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), sas_code: Some(sas_code.into()), confirmed: true, }; message.validate()?; Ok(message) }
pub fn denied(pairing_id: impl Into<String>) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), sas_code: None, confirmed: false, }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?;
match (self.confirmed, self.sas_code.as_deref()) { (true, Some(code)) => validate_sas_code(code), (true, None) => Err(eros::error!(PairingError::MissingSasCode)), (false, None) => Ok(()), (false, Some(_)) => Err(eros::error!(PairingError::SasCodeOnDeniedConfirmation)), } }}
#[derive(Clone, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingAccepted { pub pairing_id: String, pub session_token: String, pub expires_at_ms: u64,}
impl std::fmt::Debug for PairingAccepted { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter .debug_struct("PairingAccepted") .field("pairing_id", &self.pairing_id) .field("session_token", &"<redacted>") .field("expires_at_ms", &self.expires_at_ms) .finish() }}
impl PairingAccepted { pub fn new( pairing_id: impl Into<String>, session_token: impl Into<String>, expires_at_ms: u64, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), session_token: session_token.into(), expires_at_ms, }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id)?; validate_hex_256("session_token", &self.session_token)?; validate_expiry(self.expires_at_ms) }}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]#[serde(rename_all = "snake_case")]pub enum PairingRejectionReason { CodeMismatch, FingerprintMismatch, InvalidNonce, UnsupportedProtocol, UserDenied, Timeout, AlreadyInProgress, ProtocolViolation,}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingRejected { pub pairing_id: String, pub reason: PairingRejectionReason, pub retryable: bool,}
impl PairingRejected { pub fn new( pairing_id: impl Into<String>, reason: PairingRejectionReason, retryable: bool, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), reason, retryable, }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id) }}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]#[serde(rename_all = "snake_case")]pub enum PairingCancellationReason { UserRequested, Timeout, PeerDisconnected, BackendShutdown,}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PairingCancelled { pub pairing_id: String, pub reason: PairingCancellationReason,}
impl PairingCancelled { pub fn new( pairing_id: impl Into<String>, reason: PairingCancellationReason, ) -> eros::Result<Self> { let message = Self { pairing_id: pairing_id.into(), reason, }; message.validate()?; Ok(message) }
pub fn validate(&self) -> eros::Result<()> { validate_pairing_id(&self.pairing_id) }}
fn validate_pairing_id(value: &str) -> eros::Result<()> { validate_identifier("pairing_id", value)}
fn validate_identifier(field: &'static str, value: &str) -> eros::Result<()> { let limits = ProtocolLimits::default(); limits.validate_string(field, value)?;
if value != value.trim() { return Err(eros::error!(PairingError::UnexpectedWhitespace { field })); } if value.chars().any(char::is_control) { return Err(eros::error!(PairingError::ControlCharacter { field })); } if value.chars().any(|character| { !character.is_ascii_alphanumeric() && !matches!(character, '.' | '_' | '-' | ':') }) { return Err(eros::error!(PairingError::InvalidIdentifierCharacter { field })); }
Ok(())}
fn validate_hex_256(field: &'static str, value: &str) -> eros::Result<()> { if value.len() != HEX_256_LENGTH { return Err(eros::error!(PairingError::InvalidHexLength { field, length: value.len(), expected: HEX_256_LENGTH, })); } if !value .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) { return Err(eros::error!(PairingError::InvalidHexValue { field })); }
Ok(())}
fn validate_sas_code(value: &str) -> eros::Result<()> { if value.len() != SAS_CODE_LENGTH { return Err(eros::error!(PairingError::InvalidSasCodeLength { length: value.len(), expected: SAS_CODE_LENGTH, })); } if !value.bytes().all(|byte| byte.is_ascii_digit()) { return Err(eros::error!(PairingError::InvalidSasCodeValue)); }
Ok(())}
fn validate_expiry(expires_at_ms: u64) -> eros::Result<()> { if expires_at_ms == 0 { return Err(eros::error!(PairingError::ZeroExpiry)); } Ok(())}
fn validate_protocol_versions(versions: &[u16]) -> eros::Result<()> { let limits = ProtocolLimits::default(); if versions.is_empty() { return Err(eros::error!(PairingError::EmptyProtocolVersions)); } limits.validate_batch("protocol_versions", versions.len())?;
let mut seen = HashSet::with_capacity(versions.len()); for version in versions { if *version == 0 { return Err(eros::error!(PairingError::InvalidProtocolVersion { version: *version })); } if !seen.insert(version) { return Err(eros::error!(PairingError::DuplicateProtocolVersion { version: *version })); } }
Ok(())}
#[derive(Debug, PartialEq, Eq)]pub enum PairingError { EmptyProtocolVersions, InvalidProtocolVersion { version: u16, }, DuplicateProtocolVersion { version: u16, }, InvalidHexLength { field: &'static str, length: usize, expected: usize, }, InvalidHexValue { field: &'static str, }, InvalidSasCodeLength { length: usize, expected: usize, }, InvalidSasCodeValue, MissingSasCode, SasCodeOnDeniedConfirmation, ZeroExpiry, UnexpectedWhitespace { field: &'static str, }, ControlCharacter { field: &'static str, }, InvalidIdentifierCharacter { field: &'static str, },}
impl Display for PairingError { fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::EmptyProtocolVersions => { write!( formatter, "pairing must advertise at least one protocol version" ) } Self::InvalidProtocolVersion { version } => { write!(formatter, "pairing protocol version {version} is invalid") } Self::DuplicateProtocolVersion { version } => { write!( formatter, "pairing protocol version {version} is duplicated" ) } Self::InvalidHexLength { field, length, expected, } => write!( formatter, "pairing field {field} has {length} hex characters; expected {expected}" ), Self::InvalidHexValue { field } => { write!( formatter, "pairing field {field} is not canonical lowercase hex" ) } Self::InvalidSasCodeLength { length, expected } => write!( formatter, "pairing SAS code has {length} characters; expected {expected}" ), Self::InvalidSasCodeValue => { write!(formatter, "pairing SAS code must contain ASCII digits only") } Self::MissingSasCode => { write!(formatter, "confirmed pairing must include the SAS code") } Self::SasCodeOnDeniedConfirmation => write!( formatter, "denied pairing confirmation must not include the SAS code" ), Self::ZeroExpiry => write!(formatter, "pairing expiry must be greater than zero"), Self::UnexpectedWhitespace { field } => { write!( formatter, "pairing field {field} has unexpected surrounding whitespace" ) } Self::ControlCharacter { field } => { write!( formatter, "pairing field {field} contains a control character" ) } Self::InvalidIdentifierCharacter { field } => write!( formatter, "pairing field {field} contains an invalid identifier character" ), } }}
impl std::error::Error for PairingError {}