From 3ff2f34ebd0a83c7d194d7cf8e13dea3e23952b6 Mon Sep 17 00:00:00 2001 From: piegames Date: Thu, 29 Jul 2021 21:10:04 +0200 Subject: [PATCH 1/3] Enable WebSockets TLS by default --- Cargo.toml | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f4fc831d..1d955ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ eyre = "0.6.5" futures = "0.3.12" async-std = { version = "1.9.0", features = ["attributes", "unstable"] } -async-tungstenite = { version = "0.13.1", features = ["async-std-runtime"] } +async-tungstenite = { version = "0.13.1", features = ["async-std-runtime", "async-tls"] } # for "bin" feature clap = { version = "2.33.3", optional = true } diff --git a/src/lib.rs b/src/lib.rs index b5240e35..cb0f75f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ use std::str; /// Some mailbox server you might use. /// /// Two applications that want to communicate with each other *must* use the same mailbox server. -pub const DEFAULT_MAILBOX_SERVER: &str = "ws://relay.magic-wormhole.io:4000/v1"; +pub const DEFAULT_MAILBOX_SERVER: &str = "wss://relay.magic-wormhole.io:4000/v1"; #[derive(Debug, thiserror::Error)] #[non_exhaustive] From 3a4d638159e9122a91884d5af8088e3a46512440 Mon Sep 17 00:00:00 2001 From: piegames Date: Fri, 30 Jul 2021 23:29:25 +0200 Subject: [PATCH 2/3] Error handling: custom variant for server errors --- src/core.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core.rs b/src/core.rs index 255b2b7c..283f767a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -41,6 +41,9 @@ pub enum WormholeCoreError { /// the server sent some bullshit message order #[error("Protocol error: {}", _0)] Protocol(Box), + /// The server sent us an error message + #[error("Received error message from server: {}", _0)] + Server(Box), #[error( "Key confirmation failed. If you didn't mistype the code, \ this is a sign of an attacker guessing passwords. Please try \ @@ -397,11 +400,9 @@ pub async fn run( orig: _, }) => { // TODO maybe hanlde orig field for better messages - // Also, make this a proper error type, maybe ServerError or RemoteError - actions.push_back(Event::ShutDown(Err(WormholeCoreError::protocol(format!( - "Received error message from server: {}", - message - ))))); + actions.push_back(Event::ShutDown(Err(WormholeCoreError::Server( + message.into(), + )))); }, FromIO(InboundMessage::Pong { .. }) | FromIO(InboundMessage::Ack { .. }) => (), /* we ignore this, it's only for the timing log */ FromIO(InboundMessage::Unknown) => { From 1086181766705c4a5746f475d154692b10cfba9c Mon Sep 17 00:00:00 2001 From: piegames Date: Sat, 31 Jul 2021 21:26:57 +0200 Subject: [PATCH 3/3] Support permissions The actual log-in process is mostly untested Also add an experimental implementation for . --- Cargo.toml | 1 + src/bin/main.rs | 4 +- src/core.rs | 147 ++++++++++++++++++++----------- src/core/server_messages.rs | 169 ++++++++++++++++++++++++++++++++---- src/core/test.rs | 16 +++- src/lib.rs | 23 ++--- src/transfer.rs | 1 + 7 files changed, 279 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d955ec6..b1c5d309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ console = { version = "0.14.1", optional = true } indicatif = { version = "0.16.0", optional = true } dialoguer = { version = "0.8.0", optional = true } color-eyre = { version = "0.5.7", optional = true } +hashcash = "0.1.1" # for some tests [dev-dependencies] diff --git a/src/bin/main.rs b/src/bin/main.rs index 0998e906..9cf4bd47 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -365,7 +365,9 @@ fn enter_code() -> eyre::Result { } fn print_welcome(term: &mut Term, welcome: &magic_wormhole::WormholeWelcome) -> eyre::Result<()> { - writeln!(term, "Got welcome from server: {}", &welcome.welcome)?; + if let Some(welcome) = &welcome.welcome { + writeln!(term, "Got welcome from server: {}", welcome)?; + } Ok(()) } diff --git a/src/core.rs b/src/core.rs index 283f767a..48d289ac 100644 --- a/src/core.rs +++ b/src/core.rs @@ -44,6 +44,11 @@ pub enum WormholeCoreError { /// The server sent us an error message #[error("Received error message from server: {}", _0)] Server(Box), + #[error( + "Server wants one of {:?} for permissions, but we don't suppport any of these", + _0 + )] + Login(Vec), #[error( "Key confirmation failed. If you didn't mistype the code, \ this is a sign of an attacker guessing passwords. Please try \ @@ -81,11 +86,16 @@ impl From for WormholeCoreError { // TODO manually implement Debug again to display some Vec as string and others as hex #[derive(Debug, derive_more::Display)] pub enum APIEvent { - #[display(fmt = "ConnectedToServer {{ welcome: {}, code: {} }}", welcome, code)] + #[display( + fmt = "ConnectedToServer {{ motd: {} }}", + r#"motd.as_deref().unwrap_or("")"# + )] ConnectedToServer { /// A little welcome message from the server (message of the day and such) - // TODO we can actually provide more structure than a "value", see the protocol - welcome: serde_json::Value, + motd: Option, + }, + #[display(fmt = "GotCode {{ code: {} }}", code)] + GotCode { /// Share this with your peer so they can connect code: Code, }, @@ -126,6 +136,12 @@ pub enum Mood { #[derive(Debug, derive_more::Display)] enum State { + #[display(fmt = "")] // TODO + WaitForWelcome { + versions: serde_json::Value, + code_provider: CodeProvider, + }, + #[display( fmt = "AllocatingNameplate {{ wordlist: <{} words>, side: {}, versions: {} }}", "wordlist.num_words", @@ -200,39 +216,12 @@ pub async fn run( let mut actions: VecDeque = VecDeque::new(); - /* Bootstrapping code */ - let mut state; - actions.push_back(OutboundMessage::bind(appid.clone(), side.clone()).into()); - /* A mini state machine to track that messaage. It's okay for now, but modularize if it starts growing. */ - let mut welcome_message = None; - - match code_provider { - CodeProvider::AllocateCode(num_words) => { - // TODO: provide choice of wordlists - let wordlist = Arc::new(wordlist::default_wordlist(num_words)); - actions.push_back(OutboundMessage::Allocate.into()); - - state = State::AllocatingNameplate { - wordlist, - side: side.clone(), - versions, - }; - }, - CodeProvider::SetCode(code) => { - let code_string = code.to_string(); - let nc: Vec<&str> = code_string.splitn(2, '-').collect(); - let nameplate = Nameplate::new(nc[0]); - actions.push_back(OutboundMessage::claim(nameplate.clone()).into()); - - state = State::ClaimingNameplate { - nameplate, - code: Code(code), - side: side.clone(), - versions, - }; - }, - } + let mut state = State::WaitForWelcome { + versions, + code_provider, + }; + /* The usual main loop */ loop { let e = match actions.pop_front() { Some(event) => Ok(event), @@ -269,7 +258,72 @@ pub async fn run( use self::{events::Event::*, server_messages::InboundMessage}; match e { FromIO(InboundMessage::Welcome { welcome }) => { - welcome_message = Some(welcome); + match state { + State::WaitForWelcome { + versions, + code_provider, + } => { + use server_messages::{PermissionRequired, SubmitPermission}; + + actions + .push_back(APIEvent::ConnectedToServer { motd: welcome.motd }.into()); + + match welcome.permission_required { + Some(PermissionRequired { + hashcash: Some(hashcash), + .. + }) => { + let token = hashcash::Token::new(hashcash.resource, hashcash.bits); + actions.push_back( + OutboundMessage::SubmitPermission(SubmitPermission::Hashcash { + stamp: token.to_string(), + }) + .into(), + ) + }, + Some(PermissionRequired { none: true, .. }) => (), + Some(PermissionRequired { other, .. }) => { + /* We can't actually log in :/ */ + actions.push_back(Event::ShutDown(Err(WormholeCoreError::Login( + // TODO use `into_keys` once stable and remove the `cloned` + other.keys().cloned().collect(), + )))); + }, + None => (), + } + + actions + .push_back(OutboundMessage::bind(appid.clone(), side.clone()).into()); + + match code_provider { + CodeProvider::AllocateCode(num_words) => { + // TODO: provide choice of wordlists + let wordlist = Arc::new(wordlist::default_wordlist(num_words)); + actions.push_back(OutboundMessage::Allocate.into()); + + state = State::AllocatingNameplate { + wordlist, + side: side.clone(), + versions, + }; + }, + CodeProvider::SetCode(code) => { + let code_string = code.to_string(); + let nc: Vec<&str> = code_string.splitn(2, '-').collect(); + let nameplate = Nameplate::new(nc[0]); + actions.push_back(OutboundMessage::claim(nameplate.clone()).into()); + + state = State::ClaimingNameplate { + nameplate, + code: Code(code), + side: side.clone(), + versions, + }; + }, + } + }, + _ => unreachable!(), + } }, FromIO(InboundMessage::Claimed { mailbox }) => { match state { @@ -291,19 +345,7 @@ pub async fn run( &code, ))); - actions.push_back( - APIEvent::ConnectedToServer { - /* TODO Is the welcome message mandatory or optional? */ - welcome: welcome_message - .take() - .ok_or_else(|| { - anyhow::format_err!("Didn't get a welcome message") - }) - .unwrap(), - code, - } - .into(), - ); + actions.push_back(APIEvent::GotCode { code }.into()); }, State::Closing { .. } => { /* This may happen. Ignore it. */ }, _ => { @@ -420,6 +462,13 @@ pub async fn run( } }, ShutDown(result) => match state { + State::WaitForWelcome { .. } => { + state = State::Closing { + await_nameplate_release: false, + await_mailbox_close: false, + result, + }; + }, State::AllocatingNameplate { .. } => { state = State::Closing { await_nameplate_release: false, diff --git a/src/core/server_messages.rs b/src/core/server_messages.rs index b1a95fb9..115211cf 100644 --- a/src/core/server_messages.rs +++ b/src/core/server_messages.rs @@ -3,18 +3,98 @@ use super::{ util, Mood, WormholeCoreError, }; use serde_derive::{Deserialize, Serialize}; -use serde_json::{self, Value}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Nameplate { pub id: String, } +#[derive(Serialize, Debug, PartialEq, Eq, derive_more::Display)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "method")] +pub enum SubmitPermission { + #[display(fmt = "Hashcash {{ stamp: '{}' }}", stamp)] + Hashcash { stamp: String }, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Default)] +pub struct WelcomeMessage { + #[deprecated(note = "This is for the Python client")] + pub current_cli_version: Option, + pub motd: Option, + #[deprecated(note = "Servers should send a proper error message instead")] + pub error: Option, + #[serde(rename = "permission-required")] + pub permission_required: Option, +} + +impl std::fmt::Display for WelcomeMessage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "WelcomeMessage {{ ")?; + if let Some(motd) = &self.motd { + write!(f, "motd: '{}', ", motd)?; + } + if let Some(permission_required) = &self.permission_required { + write!(f, "permission_required: '{}', ", permission_required)?; + } + write!(f, ".. }}")?; + Ok(()) + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq)] +pub struct PermissionRequired { + #[serde(deserialize_with = "PermissionRequired::deserialize_none")] + pub none: bool, + pub hashcash: Option, + #[serde(flatten)] + pub other: HashMap, +} + +impl PermissionRequired { + fn deserialize_none<'de, D>(de: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: Option> = + serde::Deserialize::deserialize(de)?; + Ok(value.is_some()) + } +} + +impl std::fmt::Display for PermissionRequired { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let none_iter = std::iter::once("none").filter(|_| self.none); + let hashcash_iter = std::iter::once("hashcash").filter(|_| self.hashcash.is_some()); + let other_iter = self.other.keys().map(String::as_str); + write!( + f, + "PermissionRequired {{ one of: {:?}}}", + none_iter.chain(hashcash_iter).chain(other_iter) + ) + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq, derive_more::Display)] +#[display( + fmt = "HashcashPermission {{ bits: {}, resource: '{}' }}", + bits, + resource +)] +#[serde(deny_unknown_fields)] +pub struct HashcashPermission { + pub bits: u32, + pub resource: String, +} + // Client sends only these -#[derive(Serialize, Deserialize, Debug, PartialEq, derive_more::Display)] +#[derive(Serialize, Debug, PartialEq, derive_more::Display)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum OutboundMessage { + #[display(fmt = "SubmitPermission({})", _0)] + SubmitPermission(SubmitPermission), #[display(fmt = "Bind {{ appid: {}, side: {} }}", appid, side)] Bind { appid: AppID, @@ -91,13 +171,13 @@ impl OutboundMessage { } // Server sends only these -#[derive(Serialize, Deserialize, Debug, PartialEq, derive_more::Display)] +#[derive(Deserialize, Debug, PartialEq, derive_more::Display)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum InboundMessage { #[display(fmt = "Welcome({})", welcome)] Welcome { - welcome: Value, // left mostly-intact for application + welcome: WelcomeMessage, }, #[display(fmt = "Nameplates({:?})", nameplates)] Nameplates { @@ -146,7 +226,7 @@ pub fn deserialize(s: &str) -> Result { #[cfg(test)] mod test { use super::*; - use serde_json::{from_str, json}; + use serde_json::{from_str, json, Value}; #[test] fn test_bind() { @@ -252,23 +332,39 @@ mod test { } #[test] + #[allow(deprecated)] fn test_welcome3() { let s = r#"{"type": "welcome", "welcome": {}, "server_tx": 1234.56}"#; let m = deserialize(&s).unwrap(); - match m { - InboundMessage::Welcome { welcome: msg } => assert_eq!(msg, json!({})), - _ => panic!(), - } + assert!(matches!( + m, + InboundMessage::Welcome { + welcome: WelcomeMessage { + current_cli_version: None, + motd: None, + error: None, + permission_required: None + } + } + )); } #[test] + #[allow(deprecated)] fn test_welcome4() { let s = r#"{"type": "welcome", "welcome": {} }"#; let m = deserialize(&s).unwrap(); - match m { - InboundMessage::Welcome { welcome: msg } => assert_eq!(msg, json!({})), - _ => panic!(), - } + assert!(matches!( + m, + InboundMessage::Welcome { + welcome: WelcomeMessage { + current_cli_version: None, + motd: None, + error: None, + permission_required: None + } + } + )); } // TODO: when "error_on_line_overflow=false" lands on rustfmt(stable), @@ -278,11 +374,48 @@ mod test { fn test_welcome5() { let s = r#"{"type": "welcome", "welcome": { "motd": "hello world" }, "server_tx": 1234.56 }"#; let m = deserialize(&s).unwrap(); - match m { - InboundMessage::Welcome { welcome: msg } => - assert_eq!(msg, json!({"motd": "hello world"})), - _ => panic!(), - } + assert!(matches!(m, InboundMessage::Welcome { welcome: WelcomeMessage { current_cli_version: None, motd: Some(_), error: None, permission_required: None } })); + } + + /// Test permission_required field deserialization + #[test] + #[allow(deprecated)] + fn test_welcome6() { + let s = r#"{"type": "welcome", "welcome": { "motd": "hello world", "permission-required": { "none": {}, "hashcash": { "bits": 6, "resource": "resource-string" }, "dark-ritual": { "hocrux": true } } } }"#; + let m = deserialize(&s).unwrap(); + assert_eq!( + m, + InboundMessage::Welcome { + welcome: WelcomeMessage { + motd: Some("hello world".into()), + permission_required: Some(PermissionRequired { + none: true, + hashcash: Some(HashcashPermission { + bits: 6, + resource: "resource-string".into(), + }), + // TODO replace with array once stable + other: vec![("dark-ritual".to_string(), json!({ "hocrux": true }))] + .into_iter() + .collect() + }), + current_cli_version: None, + error: None, + } + } + ) + } + + #[test] + fn test_submit_permissions() { + let m = OutboundMessage::SubmitPermission(SubmitPermission::Hashcash { + stamp: "stamp".into(), + }); + let s = serde_json::to_string(&m).unwrap(); + assert_eq!( + s, + r#"{"type":"submit-permission","method":"hashcash","stamp":"stamp"}"# + ); } #[test] diff --git a/src/core/test.rs b/src/core/test.rs index 078739fb..4c136eef 100644 --- a/src/core/test.rs +++ b/src/core/test.rs @@ -37,7 +37,9 @@ pub async fn test_file_rust2rust() -> eyre::Result<()> { &mut None, ) .await?; - log::info!("Got welcome: {}", &welcome.welcome); + if let Some(welcome) = &welcome.welcome { + log::info!("Got welcome: {}", welcome); + } log::info!("This wormhole's code is: {}", &welcome.code); code_tx.send(welcome.code.0).unwrap(); let mut w = connector.connect_to_client().await?; @@ -73,7 +75,9 @@ pub async fn test_file_rust2rust() -> eyre::Result<()> { &mut None, ) .await?; - log::info!("Got welcome: {}", &welcome.welcome); + if let Some(welcome) = &welcome.welcome { + log::info!("Got welcome: {}", welcome); + } let mut w = connector.connect_to_client().await?; log::info!("Got key: {}", &w.key); @@ -362,7 +366,9 @@ pub async fn test_wrong_code() -> eyre::Result<()> { &mut None, ) .await?; - log::info!("Got welcome: {}", &welcome.welcome); + if let Some(welcome) = &welcome.welcome { + log::info!("Got welcome: {}", welcome); + } log::info!("This wormhole's code is: {}", &welcome.code); code_tx.send(welcome.code.0).unwrap(); @@ -385,7 +391,9 @@ pub async fn test_wrong_code() -> eyre::Result<()> { &mut None, ) .await?; - log::info!("Got welcome: {}", &welcome.welcome); + if let Some(welcome) = &welcome.welcome { + log::info!("Got welcome: {}", welcome); + } let result = connector.connect_to_client().await; /* This should have failed, due to the wrong code */ diff --git a/src/lib.rs b/src/lib.rs index cb0f75f6..08b301d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ pub enum WormholeError { /// Set a code, or allocate one #[non_exhaustive] +#[derive(Debug, Clone)] pub enum CodeProvider { /// Allocate a code with n random words AllocateCode(usize), @@ -173,7 +174,9 @@ impl WormholeConnector { loop { use self::APIEvent::*; match self.rx_core_to_api.next().await { - Some(ConnectedToServer { .. }) | Some(GotMessage(_)) => unreachable!(), + Some(ConnectedToServer { .. }) | Some(GotMessage(_)) | Some(GotCode { .. }) => { + unreachable!() + }, Some(ConnectedToClient { key: k, verifier: v, @@ -287,8 +290,8 @@ impl Wormhole { */ pub struct WormholeWelcome { pub code: Code, - /** A welcome message from the server (think of "message of the day"). Display it to the user if you wish. */ - pub welcome: String, + /** A welcome message from the server (think of "message of the day"). Should be displayed to the user if present. */ + pub welcome: Option, } /** @@ -337,19 +340,19 @@ pub async fn connect_to_server( } let code; - let welcome; + let mut welcome = None; use futures::StreamExt; loop { use self::APIEvent::*; match rx_core_to_api.next().await { - Some(ConnectedToServer { - welcome: w, - code: c, - }) => { + Some(ConnectedToServer { motd: w }) => { debug!("Got welcome"); - welcome = w; + welcome = Some(w); + }, + Some(GotCode { code: c }) => { + debug!("Got code"); code = c; break; }, @@ -363,7 +366,7 @@ pub async fn connect_to_server( Ok(( WormholeWelcome { code, - welcome: welcome.to_string(), // TODO don't do that + welcome: welcome.unwrap(), }, WormholeConnector { tx_api_to_core, diff --git a/src/transfer.rs b/src/transfer.rs index 43f2bdaf..4830b3f6 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -10,6 +10,7 @@ use futures::{AsyncRead, AsyncWrite}; use serde_derive::{Deserialize, Serialize}; +#[cfg(test)] use serde_json::json; use std::sync::Arc;