Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions s2energy-connection/examples/pairing-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ async fn main() {
.with(EnvFilter::from_default_env())
.init();

let server = Server::new(ServerConfig {
root_certificate: None,
});
let server = Server::new(ServerConfig { root_certificate: None });
let config = NodeConfig::builder(
S2NodeDescription {
id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
Expand Down
2 changes: 1 addition & 1 deletion s2energy-connection/src/pairing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ use wire::{HmacChallenge, HmacChallengeResponse};

pub use client::{Client, ClientConfig, PairingRemote};
pub use error::{ConfigError, Error, ErrorKind};
pub use server::{PairingToken, PendingPairing, RepeatedPairing, Server, ServerConfig};
pub use server::{PairingToken, PairingTokenError, PendingPairing, RepeatedPairing, Server, ServerConfig};
pub use wire::PairingS2NodeId;

use crate::{
Expand Down
64 changes: 64 additions & 0 deletions s2energy-connection/src/pairing/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(unused)]
use std::{
collections::HashMap,
str::FromStr,
sync::{Arc, Mutex},
time::Duration,
};
Expand All @@ -11,6 +12,8 @@ use axum::{
http::HeaderMap,
routing::{get, post},
};
use base64::{DecodeError, Engine, display::Base64Display, prelude::BASE64_STANDARD};
use rand::RngCore;
use reqwest::StatusCode;
use rustls::pki_types::CertificateDer;
use sha2::Digest;
Expand All @@ -32,8 +35,62 @@ const PERMANENT_PAIRING_BUFFER_SIZE: usize = 1;
/// Token known to both S2 nodes trying to pair.
///
/// This token is used to validate the identity of the nodes.
#[derive(Debug, Clone)]
pub struct PairingToken(pub Box<[u8]>);

impl std::fmt::Display for PairingToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Base64Display::new(&self.0, &BASE64_STANDARD).fmt(f)
}
}

/// Error that occurred when parsing a pairing token.
#[derive(Debug, PartialEq, Eq)]
pub struct PairingTokenError(DecodeError);

impl std::fmt::Display for PairingTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid pairing token: {}", self.0)
}
}

impl std::error::Error for PairingTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}

impl From<DecodeError> for PairingTokenError {
fn from(value: DecodeError) -> Self {
Self(value)
}
}

impl FromStr for PairingToken {
type Err = PairingTokenError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(PairingToken(BASE64_STANDARD.decode(s)?.into()))
}
}

impl PairingToken {
/// Generate a new pairing token suitable for short-lived use.
#[expect(clippy::new_without_default, reason = "Uses non-trivial randomness")]
pub fn new() -> Self {
let mut result = Self(Box::new([0; 9]));
rand::rng().fill_bytes(&mut result.0);
result
}

/// Generate a new pairing token suitable for long-term use.
pub fn new_static() -> Self {
let mut result = Self(Box::new([0; 12]));
rand::rng().fill_bytes(&mut result.0);
result
}
}

/// Server for handling S2 pairing transactions.
///
/// Responsible for providing the HTTP endpoints needed for handling
Expand Down Expand Up @@ -605,6 +662,13 @@ mod tests {
},
};

#[test]
fn token_encode_decode() {
let token = PairingToken(Box::new([0, 1, 2, 3, 4, 5, 6, 7, 8]));
assert_eq!(token.to_string(), "AAECAwQFBgcI");
assert_eq!(token.0, "AAECAwQFBgcI".parse::<PairingToken>().unwrap().0);
}

#[tokio::test]
async fn version_negotiation() {
let server = Server::new(ServerConfig { root_certificate: None });
Expand Down
Loading