use std::{borrow::Cow, str::FromStr, sync::Arc, time::Duration};
use blake2::{
digest::consts::{U32, U8},
Blake2b, Digest,
};
use magic_wormhole::{
rendezvous::{RendezvousError, DEFAULT_RENDEZVOUS_SERVER},
transit::{self, Abilities, Hints, RelayHint, Transit},
AppConfig, AppID, Code, MailboxConnection, Nameplate, Password, Wormhole, WormholeError,
};
use rust_base58::ToBase58;
use serde::{Deserialize, Serialize};
use crate::protocol::{ControllerId, IdString, Party};
#[derive(Clone, Serialize, Deserialize)]
pub struct WormholeAppVersion {}
pub const APP_CONFIG: AppConfig<WormholeAppVersion> = AppConfig {
id: AppID(Cow::Borrowed("org.timmc.breakmancer")),
rendezvous_url: Cow::Borrowed(DEFAULT_RENDEZVOUS_SERVER),
app_version: WormholeAppVersion {},
};
pub const TRANSIT_KEY_PURPOSE: &str = "org.timmc.breakmancer/main_channel";
type Blake2DeriveNameplate = Blake2b<U8>;
type Blake2DerivePassword = Blake2b<U32>;
fn derive_rendezvous_code_from_secret(shared_secret: &[u8]) -> Code {
let context_nameplate = String::from("wormhole-derive-code-nameplate");
let context_password = String::from("wormhole-derive-code-password");
let mut np_hasher = Blake2DeriveNameplate::new();
np_hasher.update(format!("{context_nameplate}|").as_bytes());
np_hasher.update(shared_secret);
let np_digest = np_hasher.finalize().into();
let nameplate_int = i64::from_le_bytes(np_digest) & 0x7FFF_FFFF_FFFF_FFFF;
let nameplate = Nameplate::from_str(&nameplate_int.to_string()).unwrap();
let mut pass_hasher = Blake2DerivePassword::new();
pass_hasher.update(format!("{context_password}|").as_bytes());
pass_hasher.update(shared_secret);
let pass_digest = pass_hasher.finalize();
let pass_encoded = pass_digest.to_base58();
let password = Password::from_str(&pass_encoded)
.map_err(|err| format!("Could not generate wormhole password from shared secret: {err}"))
.unwrap();
Code::from_components(nameplate, password)
}
fn derive_rendezvous_code(controller_id: &ControllerId) -> Code {
derive_rendezvous_code_from_secret(controller_id.id_string().as_bytes())
}
#[derive(Clone, Debug, Deserialize, Serialize)]
enum BootstrapMsg {
ControllerBootstrap(ControllerBootstrap),
BreakpointBootstrap(BreakpointBootstrap),
}
impl BootstrapMsg {
fn encode(&self) -> Vec<u8> {
serde_cbor::to_vec(self).unwrap()
}
fn decode(raw: &[u8]) -> Result<BootstrapMsg, String> {
serde_cbor::from_slice(raw)
.map_err(|err| format!("Could not decode transit bootstrap message: {err}"))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct TransitNegotiate {
abilities: Abilities,
hints: Hints,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct ControllerBootstrap {
transit_negotiate: TransitNegotiate,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct BreakpointBootstrap {
transit_negotiate: TransitNegotiate,
}
#[allow(deprecated)] async fn create_transit(mut wormhole: Wormhole, party: Party) -> Result<Transit, String> {
let transit_key = wormhole
.key()
.derive_subkey_from_purpose(TRANSIT_KEY_PURPOSE);
let my_abilities = Abilities::ALL;
let my_relay_hints =
vec![RelayHint::from_urls(None, [transit::DEFAULT_RELAY_SERVER.parse().unwrap()]).unwrap()];
let tc = transit::init(my_abilities, None, my_relay_hints.clone())
.await
.map_err(|err| format!("Unable to initialize wormhole transit: {err}"))?;
let transit_negotiate = TransitNegotiate {
abilities: my_abilities,
hints: tc.our_hints().as_ref().clone(), };
let my_intro_msg = match party {
Party::Controller => {
BootstrapMsg::ControllerBootstrap(ControllerBootstrap { transit_negotiate })
}
Party::Breakpoint => {
BootstrapMsg::BreakpointBootstrap(BreakpointBootstrap { transit_negotiate })
}
};
wormhole
.send(my_intro_msg.encode())
.await
.map_err(|err| format!("Could not send transit intro: {err}"))?;
let their_intro_raw = wormhole
.receive()
.await
.map_err(|err| format!("Could not receive transit hints: {err}"))?;
let their_intro_msg = BootstrapMsg::decode(&their_intro_raw)?;
let their_intro = match (&party, &their_intro_msg) {
(
Party::Controller,
BootstrapMsg::BreakpointBootstrap(BreakpointBootstrap { transit_negotiate }),
) => transit_negotiate,
(
Party::Breakpoint,
BootstrapMsg::ControllerBootstrap(ControllerBootstrap { transit_negotiate }),
) => transit_negotiate,
_ => Err(format!(
"Unexpected message instead of hints: {their_intro_msg:?}"
))?,
};
wormhole
.close()
.await
.map_err(|err| format!("Failed to close wormhole during transit setup: {err}"))?;
let is_leader = match party {
Party::Controller => true,
Party::Breakpoint => false,
};
let (transit, transit_info) = tc
.connect(
is_leader,
transit_key,
their_intro.abilities,
Arc::new(their_intro.hints.clone()),
)
.await
.map_err(|err| format!("Unable to connect wormhole transit: {err}"))?;
println!("Connected to wormhole transit: {transit_info}");
Ok(transit)
}
pub struct WormholeListener {
code: Code,
}
impl WormholeListener {
pub fn new(controller_id: &ControllerId) -> WormholeListener {
let code = derive_rendezvous_code(controller_id);
WormholeListener { code }
}
pub fn describe_listening(&self) -> String {
String::from("listening via magic-wormhole")
}
pub async fn accept_connection(&mut self) -> Result<WormholeTransport, String> {
let mc = MailboxConnection::connect(APP_CONFIG, self.code.clone(), true)
.await
.map_err(|err| format!("Unable to connect to rendezvous server: {err}"))?;
if let Some(motd) = mc.welcome() {
println!("Message from wormhole server:\n\n{motd}\n");
}
let wormhole = Wormhole::connect(mc)
.await
.map_err(|err| format!("Unable to connect to wormhole: {err}"))?;
let transit = create_transit(wormhole, Party::Controller).await?;
Ok(WormholeTransport {
data_channel: transit,
})
}
}
pub struct WormholeCaller {
code: Code,
}
impl WormholeCaller {
pub fn new(controller_id: &ControllerId) -> WormholeCaller {
let code = derive_rendezvous_code(controller_id);
WormholeCaller { code }
}
pub async fn new_connection(&self) -> Result<WormholeTransport, String> {
let mut attempt = 0;
let max_attempt = 35;
let mut wait_s = 0;
let max_wait_s = 60;
let mc = loop {
if attempt >= max_attempt {
println!("Made {attempt} attempts, giving up.");
Err("Gave up on connecting to controller.".to_string())?
} else {
attempt += 1;
}
wait_s = (wait_s * 2).clamp(1, max_wait_s);
match MailboxConnection::connect(APP_CONFIG, self.code.clone(), false).await {
Ok(mc) => break mc,
Err(WormholeError::UnclaimedNameplate(_)) => {
println!(
"Controller not currently listening for connections; waiting {wait_s} seconds."
);
tokio::time::sleep(Duration::from_secs(wait_s)).await;
continue;
}
Err(WormholeError::ServerError(RendezvousError::Server(err))) => {
if &*err == "crowded" {
println!(
"Wormhole nameplate busy with another breakpoint; waiting {wait_s} seconds."
);
tokio::time::sleep(Duration::from_secs(wait_s)).await;
continue;
} else {
Err(format!("Unexpected server error: {err}"))?
}
}
Err(err) => Err(format!("Unexpected error connecting to nameplate: {err}"))?,
}
};
let wormhole = Wormhole::connect(mc)
.await
.map_err(|err| format!("Unable to connect to wormhole: {err}"))?;
let transit = create_transit(wormhole, Party::Breakpoint).await?;
Ok(WormholeTransport {
data_channel: transit,
})
}
}
pub struct WormholeTransport {
data_channel: Transit,
}
impl WormholeTransport {
pub async fn receive_raw(&mut self) -> Result<Option<Vec<u8>>, String> {
match self.data_channel.receive_record().await {
Ok(data) => Ok(Some(data.to_vec())),
Err(err) => Err(format!("Could not receive data via transit: {err}")),
}
}
pub async fn send_raw(&mut self, data: &[u8]) -> Result<(), String> {
self.data_channel
.send_record(data)
.await
.map_err(|err| format!("Could not send data packet via transit: {err}"))
}
}
#[cfg(test)]
mod tests {
use magic_wormhole::transit::DirectHint;
use super::*;
#[test]
fn code_pinning() {
let id = ControllerId::from_received_string("34d1MHZZDjhn7SHG7").unwrap();
assert_eq!(
derive_rendezvous_code(&id).to_string(),
"8805933191586387843-4iYM2DsS7Nb6QX7uNwS5r6Ck1AeYofPXM5ghBhy4hvoF"
);
}
#[test]
fn bootstrap_pinning() {
let abilities = Abilities::ALL;
let hints = Hints::new(
[DirectHint::new("example.com", 12345)],
[
RelayHint::from_urls(None, [transit::DEFAULT_RELAY_SERVER.parse().unwrap()])
.unwrap(),
],
);
let msg = BootstrapMsg::ControllerBootstrap(ControllerBootstrap {
transit_negotiate: TransitNegotiate { abilities, hints },
});
assert_eq!(
msg.encode().escape_ascii().to_string(),
b"\xa1\
sControllerBootstrap\
\xa1\
qtransit_negotiate\
\xa2\
iabilities\x82\xa1dtypemdirect-tcp-v1\
\xa1\
dtypehrelay-v1ehints\x82\
\xa3\
dtypemdirect-tcp-v1hhostnamekexample.comdport\x1909\
\xa3\
dtypehrelay-v1ehints\x81\
\xa3\
hhostnamex\x19transit.magic-wormhole.iodport\x19\x0f\
\xa1\
dtypemdirect-tcp-v1dname\xf6"
.escape_ascii()
.to_string()
);
}
}