diff --git a/Cargo.toml b/Cargo.toml index b995e80..16ddef4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,8 @@ pretty-hex = { version = "0.3", default-features = false } # for non_async futures = { version = "0.3", default-features = false } +defmt = { version = "0.3", optional = true } + [features] std = ["snafu/std", "snafu/backtraces"] rsa = ["dep:rsa", "ssh-key/rsa"] @@ -71,6 +73,7 @@ rsa = ["dep:rsa", "ssh-key/rsa"] openssh-key = ["ssh-key"] # implements embedded_io::Error for sunset::Error embedded-io = ["dep:embedded-io"] +defmt = ["dep:defmt"] [dev-dependencies] # examples want std::error diff --git a/embassy/demos/common/Cargo.toml b/embassy/demos/common/Cargo.toml index cbdc68c..f4b791f 100644 --- a/embassy/demos/common/Cargo.toml +++ b/embassy/demos/common/Cargo.toml @@ -22,10 +22,20 @@ heapless = "0.7.15" # using local fork # menu = "0.3" embedded-io = { version = "0.4", features = ["async"] } +sha2 = { version = "0.10", default-features = false } +hmac = { version = "0.12", default-features = false } +# TODO: has zeroize +bcrypt = { version = "0.14", default-features = false } defmt = { version = "0.3", optional = true } log = "0.4" +pretty-hex = { version = "0.3", default-features = false } [features] -defmt = ["dep:defmt", "embassy-net/defmt", "embedded-io/defmt"] +defmt = ["dep:defmt", "embedded-io/defmt"] log = ["embassy-net/log"] + +[patch.crates-io] +embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +embassy-net-driver = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } diff --git a/embassy/demos/common/src/config.rs b/embassy/demos/common/src/config.rs index 1d7e7e1..034febc 100644 --- a/embassy/demos/common/src/config.rs +++ b/embassy/demos/common/src/config.rs @@ -1,37 +1,51 @@ #[allow(unused_imports)] -use { - sunset::error::{Error, Result, TrapBug}, -}; +use sunset::error::{Error, Result, TrapBug}; #[allow(unused_imports)] #[cfg(not(feature = "defmt"))] -use { - log::{debug, error, info, log, trace, warn}, -}; +use log::{debug, error, info, log, trace, warn}; #[allow(unused)] #[cfg(feature = "defmt")] -use defmt::{debug, info, warn, panic, error, trace}; +use defmt::{debug, error, info, panic, trace, warn}; + +use hmac::{Hmac, Mac}; +use sha2::Sha256; use heapless::{String, Vec}; use sunset_sshwire_derive::*; + use sunset::sshwire; -use sunset::sshwire::{BinString, SSHEncode, SSHDecode, WireResult, SSHSource, SSHSink, WireError}; +use sunset::sshwire::{ + BinString, SSHDecode, SSHEncode, SSHSink, SSHSource, WireError, WireResult, +}; -use sunset::{SignKey, KeyType}; use sunset::packets::Ed25519PubKey; +use sunset::{KeyType, SignKey}; + +pub const KEY_SLOTS: usize = 3; -// Be sure to bump picow flash_config::CURRENT_VERSION +// Be sure to bump CURRENT_VERSION // if this struct changes (or encode/decode impls). -#[derive(Debug, Clone)] +// BUF_SIZE will probably also need updating. +#[derive(Debug, Clone, PartialEq)] pub struct SSHConfig { pub hostkey: SignKey, - /// login password - pub pw_hash: Option<[u8; 32]>, - // 3 slots - pub auth_keys: [Option; 3], + /// login password for serial + pub console_pw: Option, + pub console_keys: [Option; KEY_SLOTS], + pub console_noauth: bool, + + /// For serial admin interface, or ssh + /// + /// If unset then serial logins are allowed without a password. + /// SSH logins are never allowed without a password. TODO add a flag + /// to disable all SSH password logins. + pub admin_pw: Option, + /// for ssh admin + pub admin_keys: [Option; KEY_SLOTS], /// SSID pub wifi_net: String<32>, @@ -40,6 +54,13 @@ pub struct SSHConfig { } impl SSHConfig { + /// Bump this when the format changes + pub const CURRENT_VERSION: u8 = 4; + /// A buffer this large will fit any SSHConfig. + // It can be updated by looking at + // `cargo test -- roundtrip_config --show-output` + pub const BUF_SIZE: usize = 443; + /// Creates a new config with default parameters. /// /// Will only fail on RNG failure. @@ -50,12 +71,41 @@ impl SSHConfig { let wifi_pw = option_env!("WIFI_PW").map(|p| p.into()); Ok(SSHConfig { hostkey, - pw_hash: None, - auth_keys: Default::default(), + console_pw: None, + console_keys: Default::default(), + console_noauth: false, + admin_pw: None, + admin_keys: Default::default(), wifi_net, wifi_pw, }) } + + pub fn set_console_pw(&mut self, pw: Option<&str>) -> Result<()> { + self.console_pw = pw.map(|p| PwHash::new(p)).transpose()?; + Ok(()) + } + + pub fn check_console_pw(&mut self, pw: &str) -> bool { + if let Some(ref p) = self.console_pw { + p.check(pw) + } else { + false + } + } + + pub fn set_admin_pw(&mut self, pw: Option<&str>) -> Result<()> { + self.admin_pw = pw.map(|p| PwHash::new(p)).transpose()?; + Ok(()) + } + + pub fn check_admin_pw(&mut self, pw: &str) -> bool { + if let Some(ref p) = self.admin_pw { + p.check(pw) + } else { + false + } + } } // a private encoding specific to demo config, not SSH defined. @@ -67,62 +117,208 @@ fn enc_signkey(k: &SignKey, s: &mut dyn SSHSink) -> WireResult<()> { } } -fn dec_signkey<'de, S>(s: &mut S) -> WireResult where S: SSHSource<'de> { +fn dec_signkey<'de, S>(s: &mut S) -> WireResult +where + S: SSHSource<'de>, +{ Ok(SignKey::Ed25519(SSHDecode::dec(s)?)) } +// encode Option as a bool then maybe a value +fn enc_option(v: &Option, s: &mut dyn SSHSink) -> WireResult<()> { + v.is_some().enc(s)?; + v.enc(s) +} + +fn dec_option<'de, S, T: SSHDecode<'de>>(s: &mut S) -> WireResult> +where + S: SSHSource<'de>, +{ + bool::dec(s)?.then(|| SSHDecode::dec(s)).transpose() +} + impl SSHEncode for SSHConfig { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + info!("enc si"); enc_signkey(&self.hostkey, s)?; - self.pw_hash.is_some().enc(s)?; - self.pw_hash.enc(s)?; + info!("enc pw"); + enc_option(&self.console_pw, s)?; - for k in self.auth_keys.iter() { - k.is_some().enc(s)?; - k.enc(s)?; + for k in self.console_keys.iter() { + info!("enc k"); + enc_option(k, s)?; } - self.wifi_net.as_str().enc(s)?; + self.console_noauth.enc(s)?; + + info!("enc ad"); + enc_option(&self.admin_pw, s)?; - self.wifi_pw.is_some().enc(s)?; - if let Some(ref p) = self.wifi_pw { - p.as_str().enc(s)?; + for k in self.admin_keys.iter() { + info!("enc ke"); + enc_option(k, s)?; } + + info!("enc net"); + self.wifi_net.as_str().enc(s)?; + info!("enc netpw"); + enc_option(&self.wifi_pw, s)?; Ok(()) } } impl<'de> SSHDecode<'de> for SSHConfig { - fn dec(s: &mut S) -> WireResult where S: SSHSource<'de> { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + info!("dec si"); let hostkey = dec_signkey(s)?; - let have_pw_hash = bool::dec(s)?; - let pw_hash = have_pw_hash.then(|| SSHDecode::dec(s)).transpose()?; + info!("dec pw"); + let console_pw = dec_option(s)?; - let mut auth_keys = [None, None, None]; - for k in auth_keys.iter_mut() { - if bool::dec(s)? { - *k = Some(SSHDecode::dec(s)).transpose()?; - } + let mut console_keys = [None, None, None]; + for k in console_keys.iter_mut() { + info!("dec k"); + *k = dec_option(s)?; } - let wifi_net = <&str>::dec(s)?.into(); - let have_wifi_pw = bool::dec(s)?; + let console_noauth = SSHDecode::dec(s)?; + + info!("dec ad"); + let admin_pw = dec_option(s)?; + + let mut admin_keys = [None, None, None]; + for k in admin_keys.iter_mut() { + info!("dec adk"); + *k = dec_option(s)?; + } + + info!("dec wn"); + let wifi_net = SSHDecode::dec(s)?; + info!("dec wp"); + let wifi_pw = dec_option(s)?; - let wifi_pw = have_wifi_pw.then(|| { - let p: &str = SSHDecode::dec(s)?; - Ok(p.into()) - }) - .transpose()?; Ok(Self { hostkey, - pw_hash, - auth_keys, + console_pw, + console_keys, + console_noauth, + admin_pw, + admin_keys, wifi_net, wifi_pw, }) } } +/// Stores a bcrypt password hash. +/// +/// We use bcrypt because it seems the best password hashing option where +/// memory hardness isn't possible (the rp2040 is smaller than CPU or GPU memory). +/// +/// The cost is currently set to 6, taking ~500ms on a 125mhz rp2040. +/// Time converges to roughly 8.6ms * 2**cost +/// +/// Passwords are pre-hashed to avoid bcrypt's 72 byte limit. +/// rust-bcrypt allows nulls in passwords. +/// We use an hmac rather than plain hash to avoid password shucking +/// (an attacker bcrypts known hashes from some other breach, then +/// brute forces the weaker hash for any that match). +#[derive(Clone, SSHEncode, SSHDecode, PartialEq)] +pub struct PwHash { + salt: [u8; 16], + hash: [u8; 24], + cost: u8, +} + +impl PwHash { + const COST: u8 = 6; + /// `pw` must not be empty. + pub fn new(pw: &str) -> Result { + if pw.is_empty() { + return sunset::error::BadUsage.fail(); + } + + let mut salt = [0u8; 16]; + sunset::random::fill_random(&mut salt)?; + let prehash = Self::prehash(pw, &salt); + let cost = Self::COST; + let hash = bcrypt::bcrypt(cost as u32, salt, &prehash); + Ok(Self { salt, hash, cost }) + } + + pub fn check(&self, pw: &str) -> bool { + if pw.is_empty() { + return false; + } + let prehash = Self::prehash(pw, &self.salt); + let check_hash = + bcrypt::bcrypt(self.cost as u32, self.salt.clone(), &prehash); + check_hash == self.hash + } + + fn prehash(pw: &str, salt: &[u8]) -> [u8; 32] { + // OK unwrap: can't fail, accepts any length + let mut prehash = Hmac::::new_from_slice(&salt).unwrap(); + prehash.update(pw.as_bytes()); + prehash.finalize().into_bytes().into() + } +} + +impl core::fmt::Debug for PwHash { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PwHash").finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use config::PwHash; + use sunset::packets::Ed25519PubKey; + use sunset::sshwire::{self, Blob}; + + #[test] + fn roundtrip_config() { + // default config + let c1 = SSHConfig::new().unwrap(); + let mut buf = [0u8; 1000]; + let l = sshwire::write_ssh(&mut buf, &c1).unwrap(); + let v = &buf[..l]; + let c2: SSHConfig = sshwire::read_ssh(&buf, None).unwrap(); + assert_eq!(c1, c2); + // All the fruit, to check BUF_SIZE. + // Variable length fields are all max size. + let mut c1 = SSHConfig { + hostkey: c1.hostkey, + console_pw: Some(PwHash::new("zong").unwrap()), + console_keys: [ + Some(Ed25519PubKey { key: Blob([14u8; 32]) }), + Some(Ed25519PubKey { key: Blob([24u8; 32]) }), + Some(Ed25519PubKey { key: Blob([34u8; 32]) }), + ], + console_noauth: true, + admin_pw: Some(PwHash::new("f").unwrap()), + admin_keys: [ + Some(Ed25519PubKey { key: Blob([19u8; 32]) }), + Some(Ed25519PubKey { key: Blob([29u8; 32]) }), + Some(Ed25519PubKey { key: Blob([39u8; 32]) }), + ], + wifi_net: core::str::from_utf8([b'a'; 32].as_slice()).unwrap().into(), + wifi_pw: Some( + core::str::from_utf8([b'f'; 63].as_slice()).unwrap().into(), + ), + }; + + let mut buf = [0u8; SSHConfig::BUF_SIZE]; + let l = sshwire::write_ssh(&mut buf, &c1).unwrap(); + println!("BUF_SIZE must be at least {}", l); + let v = &buf[..l]; + let c2: SSHConfig = sshwire::read_ssh(&buf, None).unwrap(); + assert_eq!(c1, c2); + } +} diff --git a/embassy/demos/common/src/lib.rs b/embassy/demos/common/src/lib.rs index 7cdcecb..afb4084 100644 --- a/embassy/demos/common/src/lib.rs +++ b/embassy/demos/common/src/lib.rs @@ -1,15 +1,18 @@ -#![no_std] +#![cfg_attr(not(any(feature = "std", test)), no_std)] #![feature(type_alias_impl_trait)] #![feature(async_fn_in_trait)] // #![allow(incomplete_features)] -mod config; mod server; +pub mod config; pub mod menu; pub mod demo_menu; pub use server::{Shell, listener}; pub use config::SSHConfig; pub use demo_menu::BufOutput; + +// needed for derive +use sunset::sshwire; diff --git a/embassy/demos/common/src/menu.rs b/embassy/demos/common/src/menu.rs index 4c7afa5..5c4dd5c 100644 --- a/embassy/demos/common/src/menu.rs +++ b/embassy/demos/common/src/menu.rs @@ -113,6 +113,7 @@ where /// Maximum four levels deep menus: [Option<&'a Menu<'a, T>>; 4], depth: usize, + echo: bool, /// The context object the `Runner` carries around. pub context: T, } @@ -242,7 +243,7 @@ where /// buffer that the `Runner` can use. Feel free to pass anything as the /// `context` type - the only requirement is that the `Runner` can /// `write!` to the context, which it will do for all text output. - pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], mut context: T) -> Runner<'a, T> { + pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], echo: bool, mut context: T) -> Runner<'a, T> { if let Some(cb_fn) = menu.entry { cb_fn(&mut context); } @@ -251,6 +252,7 @@ where depth: 0, buffer, used: 0, + echo, context, }; r.prompt(true); @@ -303,6 +305,13 @@ where return; } let outcome = if input == 0x0D { + if !self.echo { + // Echo the command + write!(self.context, "\r").unwrap(); + if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { + write!(self.context, "{}", s).unwrap(); + } + } // Handle the command self.process_command(); Outcome::CommandProcessed @@ -332,8 +341,7 @@ where self.buffer[self.used] = input; self.used += 1; - // #[cfg(feature = "echo")] - { + if self.echo { // We have to do this song and dance because `self.prompt()` needs // a mutable reference to self, and we can't have that while // holding a reference to the buffer at the same time. @@ -442,7 +450,7 @@ where } } } else { - writeln!(self.context, "Input was empty?").unwrap(); + // writeln!(self.context, "Input was empty?").unwrap(); } } else { // Hmm .. we did not have a valid string diff --git a/embassy/demos/common/src/server.rs b/embassy/demos/common/src/server.rs index ab116b0..7f24db8 100644 --- a/embassy/demos/common/src/server.rs +++ b/embassy/demos/common/src/server.rs @@ -9,6 +9,9 @@ use { #[cfg(feature = "defmt")] use defmt::{debug, info, warn, panic, error, trace}; +use core::fmt::Write as _; +use pretty_hex::PrettyHex; + use embassy_sync::mutex::Mutex; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_net::tcp::TcpSocket; @@ -126,6 +129,8 @@ struct DemoServer<'a, S: Shell> { } impl<'a, S: Shell> DemoServer<'a, S> { + const ADMIN_USER: &'static str = "config"; + fn new(shell: &'a S, config: SSHConfig) -> Result { Ok(Self { @@ -135,18 +140,61 @@ impl<'a, S: Shell> DemoServer<'a, S> { shell, }) } + + fn is_admin(&self, username: TextString) -> bool { + username.as_str().unwrap_or_default() == Self::ADMIN_USER + } } impl<'a, S: Shell> ServBehaviour for DemoServer<'a, S> { + fn hostkeys(&mut self) -> BhResult> { // OK unwrap: only one element Ok(heapless::Vec::from_slice(&[&self.config.hostkey]).unwrap()) } async fn auth_unchallenged(&mut self, username: TextString<'_>) -> bool { - info!("Allowing auth for user {}", username.as_str().unwrap_or("bad")); - self.shell.authed(username.as_str().unwrap_or("")).await; - true + if !self.is_admin(username) && self.config.console_noauth { + info!("Allowing auth for user {}", username.as_str().unwrap_or("bad")); + self.shell.authed(username.as_str().unwrap_or("")).await; + true + } else { + false + } + } + + async fn auth_password(&mut self, username: TextString<'_>, password: TextString<'_>) -> bool { + let p = if self.is_admin(username) { + &self.config.admin_pw + } else { + &self.config.console_pw + }; + + if let Some(ref p) = p { + if let (Ok(user), Ok(pw)) = (username.as_str(), password.as_str()) { + if p.check(pw) { + self.shell.authed(user).await; + return true + } + } + } + false + } + + fn have_auth_password(&self, username: TextString) -> bool { + if self.is_admin(username) { + self.config.admin_pw.is_some() + } else { + self.config.console_pw.is_some() + } + } + + fn have_auth_pubkey(&self, username: TextString) -> bool { + if self.is_admin(username) { + self.config.admin_keys.iter().any(|k| k.is_some()) + } else { + self.config.console_keys.iter().any(|k| k.is_some()) + } } fn open_session(&mut self, chan: ChanHandle) -> ChanOpened { @@ -205,14 +253,15 @@ pub trait Shell { #[derive(Default)] pub struct BufOutput { /// Sufficient to hold output produced from a single keystroke input. Further output will be discarded - s: heapless::String<300>, + // pub s: String<300>, + // todo + pub s: String<3000>, } impl BufOutput { pub async fn flush(&mut self, w: &mut W) -> Result<()> where W: asynch::Write + embedded_io::Io { - let mut b = self.s.as_str().as_bytes(); while b.len() > 0 { let l = w.write(b).await?; diff --git a/embassy/demos/picow/Cargo.lock b/embassy/demos/picow/Cargo.lock index 7252ddb..d076e64 100644 --- a/embassy/demos/picow/Cargo.lock +++ b/embassy/demos/picow/Cargo.lock @@ -122,6 +122,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bcrypt" +version = "0.14.0" +dependencies = [ + "base64", + "blowfish", + "getrandom", + "subtle", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -158,6 +174,16 @@ dependencies = [ "generic-array 0.14.6", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bytemuck" version = "1.13.1" @@ -321,10 +347,10 @@ dependencies = [ "cortex-m", "cortex-m-rt", "defmt", - "embassy-futures", - "embassy-net-driver-channel", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", + "embassy-net-driver-channel 0.1.0", "embassy-sync 0.1.0", - "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time 0.1.1 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embedded-hal 1.0.0-alpha.10", "futures", "num_enum", @@ -484,7 +510,7 @@ dependencies = [ "cortex-m", "critical-section 1.1.1", "embassy-executor", - "embassy-hal-common", + "embassy-hal-common 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-macros", "embassy-sync 0.2.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", ] @@ -518,11 +544,22 @@ dependencies = [ "static_cell", ] +[[package]] +name = "embassy-futures" +version = "0.1.0" + [[package]] name = "embassy-futures" version = "0.1.0" source = "git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e#3e730aa8b06401003202bf9e21a9c83ec6b21b0e" +[[package]] +name = "embassy-hal-common" +version = "0.1.0" +dependencies = [ + "num-traits", +] + [[package]] name = "embassy-hal-common" version = "0.1.0" @@ -546,16 +583,14 @@ dependencies = [ [[package]] name = "embassy-net" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e#3e730aa8b06401003202bf9e21a9c83ec6b21b0e" dependencies = [ "as-slice 0.2.1", "atomic-polyfill 1.0.1", "atomic-pool", - "defmt", - "embassy-hal-common", - "embassy-net-driver", - "embassy-sync 0.2.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", - "embassy-time 0.1.1 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", + "embassy-hal-common 0.1.0", + "embassy-net-driver 0.1.0", + "embassy-sync 0.2.0", + "embassy-time 0.1.1", "embedded-io 0.4.0", "embedded-nal-async", "futures", @@ -566,12 +601,22 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "embassy-net-driver" +version = "0.1.0" + [[package]] name = "embassy-net-driver" version = "0.1.0" source = "git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e#3e730aa8b06401003202bf9e21a9c83ec6b21b0e" + +[[package]] +name = "embassy-net-driver-channel" +version = "0.1.0" dependencies = [ - "defmt", + "embassy-futures 0.1.0", + "embassy-net-driver 0.1.0", + "embassy-sync 0.2.0", ] [[package]] @@ -579,8 +624,8 @@ name = "embassy-net-driver-channel" version = "0.1.0" source = "git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e#3e730aa8b06401003202bf9e21a9c83ec6b21b0e" dependencies = [ - "embassy-futures", - "embassy-net-driver", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", + "embassy-net-driver 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-sync 0.2.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", ] @@ -598,8 +643,8 @@ dependencies = [ "embassy-cortex-m", "embassy-embedded-hal", "embassy-executor", - "embassy-futures", - "embassy-hal-common", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", + "embassy-hal-common 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-sync 0.2.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-time 0.1.1 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-usb-driver", @@ -633,6 +678,17 @@ dependencies = [ "heapless", ] +[[package]] +name = "embassy-sync" +version = "0.2.0" +dependencies = [ + "cfg-if", + "critical-section 1.1.1", + "embedded-io 0.4.0", + "futures-util", + "heapless", +] + [[package]] name = "embassy-sync" version = "0.2.0" @@ -661,13 +717,10 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd403e218939bba4a1fe4b58c6f81bf0818852bdd824147f95e6dc4ff4166ac4" dependencies = [ "atomic-polyfill 1.0.1", "cfg-if", "critical-section 1.1.1", - "defmt", "embedded-hal 0.2.7", "futures-util", "heapless", @@ -681,6 +734,7 @@ dependencies = [ "atomic-polyfill 1.0.1", "cfg-if", "critical-section 1.1.1", + "defmt", "embedded-hal 0.2.7", "futures-util", "heapless", @@ -692,8 +746,8 @@ version = "0.1.0" source = "git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e#3e730aa8b06401003202bf9e21a9c83ec6b21b0e" dependencies = [ "defmt", - "embassy-futures", - "embassy-net-driver-channel", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", + "embassy-net-driver-channel 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-sync 0.2.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-usb-driver", "heapless", @@ -1613,8 +1667,6 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smoltcp" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9786ac45091b96f946693e05bfa4d8ca93e2d3341237d97a380107a6b38dea" dependencies = [ "bitflags", "byteorder", @@ -1714,6 +1766,7 @@ dependencies = [ "chacha20", "cipher", "ctr", + "defmt", "digest", "embedded-io 0.4.0", "futures", @@ -1737,15 +1790,19 @@ dependencies = [ name = "sunset-demo-embassy-common" version = "0.1.0" dependencies = [ + "bcrypt", "defmt", - "embassy-futures", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-net", - "embassy-net-driver", + "embassy-net-driver 0.1.0", "embassy-sync 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time 0.1.1 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embedded-io 0.4.0", "heapless", + "hmac", "log", + "pretty-hex", + "sha2", "sunset", "sunset-embassy", "sunset-sshwire-derive", @@ -1765,12 +1822,12 @@ dependencies = [ "defmt", "defmt-rtt", "embassy-executor", - "embassy-futures", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-net", - "embassy-net-driver", + "embassy-net-driver 0.1.0", "embassy-rp", "embassy-sync 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time 0.1.1 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-usb", "embassy-usb-driver", "embedded-hal 1.0.0-alpha.10", @@ -1782,8 +1839,11 @@ dependencies = [ "log", "panic-probe", "pin-utils", + "pretty-hex", "rand", "sha2", + "smoltcp", + "snafu", "static_cell", "sunset", "sunset-demo-embassy-common", @@ -1796,7 +1856,7 @@ name = "sunset-embassy" version = "0.2.0-alpha" dependencies = [ "atomic-polyfill 1.0.1", - "embassy-futures", + "embassy-futures 0.1.0 (git+https://github.com/embassy-rs/embassy?rev=3e730aa8b06401003202bf9e21a9c83ec6b21b0e)", "embassy-sync 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "embedded-io 0.4.0", "log", diff --git a/embassy/demos/picow/Cargo.toml b/embassy/demos/picow/Cargo.toml index 5786c99..eaeee18 100644 --- a/embassy/demos/picow/Cargo.toml +++ b/embassy/demos/picow/Cargo.toml @@ -33,9 +33,12 @@ static_cell = "1.0" defmt = { version = "0.3", optional = true } defmt-rtt = "0.3" panic-probe = { version = "0.3", features = ["print-defmt"] } +pretty-hex = { version = "0.3", default-features = false } log = { version = "0.4" } futures = { version = "0.3", default-features = false } +snafu = { version = "0.7", default-features = false, features = ["rust_1_61"] } + cortex-m = { version = "0.7.6", features = ["critical-section-single-core"]} cortex-m-rt = "0.7.0" @@ -56,9 +59,12 @@ critical-section = "1.1" rand = { version = "0.8", default-features = false, features = ["getrandom"] } sha2 = { version = "0.10", default-features = false } +# for defmt feature +smoltcp = { default-features = false } + [features] default = ["defmt", "sunset-demo-embassy-common/defmt", "embassy-usb/defmt"] -defmt = ["dep:defmt"] +defmt = ["dep:defmt", "sunset/defmt", "smoltcp/defmt"] # Use cyw43 firmware already on flash. This saves time when developing. # probe-rs-cli download firmware/43439A0.bin --format bin --chip RP2040 --base-address 0x10100000 @@ -69,14 +75,20 @@ romfw = [] embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } -embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +# embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } embassy-usb = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } # for cyw43 -embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } -embassy-net-driver = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +# embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } +# embassy-net-driver = { git = "https://github.com/embassy-rs/embassy", rev = "3e730aa8b06401003202bf9e21a9c83ec6b21b0e" } + +embassy-net = { path = "/home/matt/3rd/rs/embassy/embassy-net" } +embassy-net-driver = { path = "/home/matt/3rd/rs/embassy/embassy-net-driver" } +embassy-net-driver-channel = { path = "/home/matt/3rd/rs/embassy/embassy-net-driver-channel" } +smoltcp = { path = "/home/matt/3rd/rs/smoltcp" } -# embedded-io = { path = "/home/matt/3rd/rs/embedded-io" } +bcrypt = { path = "/home/matt/3rd/rs/bcrypt" } [profile.dev] debug = 2 diff --git a/embassy/demos/picow/src/flashconfig.rs b/embassy/demos/picow/src/flashconfig.rs index ccf9d93..afd5989 100644 --- a/embassy/demos/picow/src/flashconfig.rs +++ b/embassy/demos/picow/src/flashconfig.rs @@ -24,15 +24,14 @@ use sunset::sshwire; use sunset::sshwire::{BinString, SSHEncode, SSHDecode, WireResult, SSHSource, SSHSink, WireError}; use sunset::sshwire::OwnOrBorrow; -use crate::demo_common::SSHConfig; - -// bump this when the format changes -const CURRENT_VERSION: u8 = 2; +use crate::demo_common; +use demo_common::SSHConfig; // TODO: unify offsets with wifi's romfw feature const CONFIG_OFFSET: u32 = 0x150000; pub const FLASH_SIZE: usize = 2*1024*1024; +// SSHConfig::CURRENT_VERSION must be bumped if any of this struct changes #[derive(SSHEncode, SSHDecode)] struct FlashConfig<'a> { version: u8, @@ -41,6 +40,10 @@ struct FlashConfig<'a> { hash: [u8; 32], } +impl FlashConfig<'_> { + const BUF_SIZE: usize = 1 + SSHConfig::BUF_SIZE + 32; +} + fn config_hash(config: &SSHConfig) -> Result<[u8; 32]> { let mut h = sha2::Sha256::new(); sshwire::hash_ser(&mut h, config, None)?; @@ -49,13 +52,15 @@ fn config_hash(config: &SSHConfig) -> Result<[u8; 32]> { /// Loads a SSHConfig at startup. Good for persisting hostkeys. pub fn load_or_create(flash: &mut Flash<'_, FLASH, FLASH_SIZE>) -> Result { + use snafu::Error; let c = load(flash); match load(flash) { Ok(c) => { info!("Good existing config"); return Ok(c) } - Err(c) => info!("Existing config bad, making new"), + // Err(sunset::Error::Custom(msg: msg)) => info!("Existing config bad, making new. {}", msg), + Err(e) => info!("Existing config bad, making new. {}", e.description()), } create(flash) @@ -70,11 +75,19 @@ pub fn create(flash: &mut Flash<'_, FLASH, FLASH_SIZE>) -> Result { } pub fn load(flash: &mut Flash<'_, FLASH, FLASH_SIZE>) -> Result { - let mut buf = [0u8; ERASE_SIZE]; + // let mut buf = [0u8; ERASE_SIZE]; + let mut buf = [0u8; FlashConfig::BUF_SIZE]; flash.read(CONFIG_OFFSET, &mut buf).map_err(|_| Error::msg("flash error"))?; + + use pretty_hex::PrettyHex; + use core::fmt::Write; + let mut b = demo_common::BufOutput::default(); + writeln!(b, "load {:?}", buf.hex_dump()); + info!("{}", &b.s); + let s: FlashConfig = sshwire::read_ssh(&buf, None)?; - if s.version != CURRENT_VERSION { + if s.version != SSHConfig::CURRENT_VERSION { return Err(Error::msg("wrong config version")) } @@ -94,11 +107,19 @@ pub fn load(flash: &mut Flash<'_, FLASH, FLASH_SIZE>) -> Result { pub fn save(flash: &mut Flash<'_, FLASH, FLASH_SIZE>, config: &SSHConfig) -> Result<()> { let mut buf = [0u8; ERASE_SIZE]; let sc = FlashConfig { - version: CURRENT_VERSION, + version: SSHConfig::CURRENT_VERSION, config: OwnOrBorrow::Borrow(&config), hash: config_hash(&config)?, }; - sshwire::write_ssh(&mut buf, &sc)?; + let l = sshwire::write_ssh(&mut buf, &sc)?; + let buf = &buf[..l]; + + use pretty_hex::PrettyHex; + use core::fmt::Write; + let mut b = demo_common::BufOutput::default(); + writeln!(b, "save {:?}", buf.hex_dump()); + info!("{}", &b.s); + trace!("flash erase"); flash.erase(CONFIG_OFFSET, CONFIG_OFFSET + ERASE_SIZE as u32) .map_err(|_| Error::msg("flash erase error"))?; @@ -107,7 +128,7 @@ pub fn save(flash: &mut Flash<'_, FLASH, FLASH_SIZE>, config: &SSHConfig) -> Res flash.write(CONFIG_OFFSET, &buf) .map_err(|_| Error::msg("flash write error"))?; - trace!("save done"); + info!("flash save done"); Ok(()) } diff --git a/embassy/demos/picow/src/main.rs b/embassy/demos/picow/src/main.rs index c888403..a294d07 100644 --- a/embassy/demos/picow/src/main.rs +++ b/embassy/demos/picow/src/main.rs @@ -6,56 +6,55 @@ #[allow(unused_imports)] #[cfg(not(feature = "defmt"))] -pub use { - log::{debug, error, info, log, trace, warn}, -}; +pub use log::{debug, error, info, log, trace, warn}; #[allow(unused_imports)] #[cfg(feature = "defmt")] -pub use defmt::{debug, info, warn, panic, error, trace}; +pub use defmt::{debug, error, info, panic, trace, warn}; use {defmt_rtt as _, panic_probe as _}; use core::fmt::Write as _; - +use pretty_hex::PrettyHex; use embassy_executor::Spawner; -use embassy_net::Stack; use embassy_futures::join::join; use embassy_futures::select::select; -use embassy_rp::{pio::PioPeripheral, interrupt}; +use embassy_net::Stack; use embassy_rp::peripherals::FLASH; -use embedded_io::{asynch, Io}; +use embassy_rp::{interrupt, pio::PioPeripheral}; +use embassy_time::Duration; use embedded_io::asynch::Write as _; +use embedded_io::{asynch, Io}; use heapless::{String, Vec}; use static_cell::StaticCell; use demo_common::menu::Runner as MenuRunner; -use embedded_io::asynch::Read; -use embassy_sync::signal::Signal; use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::signal::Signal; +use embedded_io::asynch::Read; use sunset::*; use sunset_embassy::{SSHServer, SunsetMutex}; -pub(crate) use sunset_demo_embassy_common as demo_common; use crate::demo_common::singleton; +pub(crate) use sunset_demo_embassy_common as demo_common; mod flashconfig; -mod wifi; -mod usbserial; mod picowmenu; mod takepipe; +mod usbserial; +mod wifi; -use demo_common::{SSHConfig, demo_menu, Shell}; +use demo_common::{demo_menu, SSHConfig, Shell}; use takepipe::TakeBase; const NUM_LISTENERS: usize = 4; // +1 for dhcp. referenced directly by wifi_stack() function -pub(crate) const NUM_SOCKETS: usize = NUM_LISTENERS+1; +pub(crate) const NUM_SOCKETS: usize = NUM_LISTENERS + 1; #[embassy_executor::main] async fn main(spawner: Spawner) { @@ -74,14 +73,9 @@ async fn main(spawner: Spawner) { flashconfig::load_or_create(&mut flash).unwrap() }; - let flash = &*singleton!( - SunsetMutex::new(flash) - ); - - let config = &*singleton!( - SunsetMutex::new(config) - ); + let flash = &*singleton!(SunsetMutex::new(flash)); + let config = &*singleton!(SunsetMutex::new(config)); let (wifi_net, wifi_pw) = { let c = config.lock().await; @@ -89,8 +83,11 @@ async fn main(spawner: Spawner) { }; // spawn the wifi stack let (_, sm, _, _, _) = p.PIO0.split(); - let (stack, wifi_control) = wifi::wifi_stack(&spawner, p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.DMA_CH0, sm, - wifi_net, wifi_pw).await; + let (stack, wifi_control) = wifi::wifi_stack( + &spawner, p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.DMA_CH0, sm, wifi_net, + wifi_pw, + ) + .await; let stack = &*singleton!(stack); let wifi_control = singleton!(SunsetMutex::new(wifi_control)); spawner.spawn(net_task(&stack)).unwrap(); @@ -98,12 +95,11 @@ async fn main(spawner: Spawner) { let usb_pipe = singleton!(takepipe::TakePipe::new()); let usb_pipe = singleton!(usb_pipe.base()); - let state = GlobalState { - usb_pipe, - wifi_control, - config, - flash, - }; + let watchdog = singleton!(SunsetMutex::new( + embassy_rp::watchdog::Watchdog::new(p.WATCHDOG) + )); + + let state = GlobalState { usb_pipe, wifi_control, config, flash, watchdog }; let state = singleton!(state); let usb_irq = interrupt::take!(USBCTRL_IRQ); @@ -116,20 +112,23 @@ async fn main(spawner: Spawner) { // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] -async fn listener(stack: &'static Stack>, +async fn listener( + stack: &'static Stack>, config: &'static SunsetMutex, - ctx: &'static GlobalState) -> ! { + ctx: &'static GlobalState, +) -> ! { demo_common::listener::<_, DemoShell>(stack, config, ctx).await } pub(crate) struct GlobalState { // If taking multiple mutexes, lock in the order below avoid inversion. - pub usb_pipe: &'static TakeBase<'static>, pub wifi_control: &'static SunsetMutex>, pub config: &'static SunsetMutex, - pub flash: &'static SunsetMutex>, + pub flash: &'static SunsetMutex< + embassy_rp::flash::Flash<'static, FLASH, { flashconfig::FLASH_SIZE }>, + >, + pub watchdog: &'static SunsetMutex, } struct DemoShell { @@ -141,29 +140,42 @@ struct DemoShell { } // `local` is set for usb serial menus which require different auth -async fn menu(mut chanr: R, mut chanw: W, +async fn menu( + mut chanr: R, + mut chanw: W, local: bool, - state: &'static GlobalState) -> Result<()> - where R: asynch::Read+Io, - W: asynch::Write+Io { + state: &'static GlobalState, +) -> Result<()> +where + R: asynch::Read + Io, + W: asynch::Write + Io, +{ let mut menu_buf = [0u8; 64]; let menu_ctx = picowmenu::MenuCtx::new(state); - let mut menu = MenuRunner::new(&picowmenu::SETUP_MENU, &mut menu_buf, menu_ctx); - - // bodge - for c in "help\r\n".bytes() { - menu.input_byte(c); + // let echo = !local; + let echo = true; + let mut menu = + MenuRunner::new(&picowmenu::SETUP_MENU, &mut menu_buf, echo, menu_ctx); + + // Bodge. Isn't safe for local serial either since Linux would reply to those + // bytes with echo (a terminal emulator isn't attached yet), and then we get + // confused by it. + if !local { + for c in "help\r\n".bytes() { + menu.input_byte(c); + } + menu.context.out.flush(&mut chanw).await?; } - menu.context.out.flush(&mut chanw).await?; 'io: loop { let mut b = [0u8; 20]; let lr = chanr.read(&mut b).await?; if lr == 0 { - break + break; } let b = &mut b[..lr]; + for c in b.iter() { menu.input_byte(*c); menu.context.out.flush(&mut chanw).await?; @@ -174,6 +186,14 @@ async fn menu(mut chanr: R, mut chanw: W, if local { writeln!(menu.context.out, "serial can't loop"); } else { + if state.usb_pipe.is_in_use() { + writeln!( + menu.context.out, + "Opening usb1, stealing existing session" + ); + } else { + writeln!(menu.context.out, "Opening usb1"); + } serial(chanr, chanw, state).await?; // TODO we could return to the menu on serial error? break 'io; @@ -181,6 +201,7 @@ async fn menu(mut chanr: R, mut chanw: W, } if menu.context.need_save { + info!("needs save"); // clear regardless of success, don't want a tight loop. menu.context.need_save = false; @@ -191,20 +212,39 @@ async fn menu(mut chanr: R, mut chanw: W, } } + if menu.context.logout { + break 'io; + } + + if menu.context.reset { + let _ = chanw.write_all(b"Resetting\r\n").await; + let mut wd = state.watchdog.lock().await; + wd.start(Duration::from_millis(200)); + loop { + embassy_time::Timer::after(Duration::from_secs(1)).await; + } + } + + // messages from handling menu.context.out.flush(&mut chanw).await?; } } Ok(()) } -async fn serial(mut chanr: R, mut chanw: W, state: &'static GlobalState) -> Result<()> - where R: asynch::Read+Io, - W: asynch::Write+Io { - +async fn serial( + mut chanr: R, + mut chanw: W, + state: &'static GlobalState, +) -> Result<()> +where + R: asynch::Read + Io, + W: asynch::Write + Io, +{ let (mut rx, mut tx) = state.usb_pipe.take().await; let r = async { // TODO: could have a single buffer to translate in-place. - const DOUBLE: usize = 2*takepipe::READ_SIZE; + const DOUBLE: usize = 2 * takepipe::READ_SIZE; let mut b = [0u8; takepipe::READ_SIZE]; let mut btrans = Vec::::new(); loop { @@ -264,21 +304,24 @@ impl Shell for DemoShell { } async fn authed(&self, username: &str) { + info!("authed for {}", username); let mut u = self.username.lock().await; *u = username.try_into().unwrap_or(String::new()); } - async fn run<'f, S: ServBehaviour>(&self, serv: &'f SSHServer<'f, S>) -> Result<()> - { + async fn run<'f, S: ServBehaviour>( + &self, + serv: &'f SSHServer<'f, S>, + ) -> Result<()> { let session = async { // wait for a shell to start let chan_handle = self.notify.wait().await; let stdio = serv.stdio(chan_handle).await?; - if *self.username.lock().await == "serial" { - serial(stdio.clone(), stdio, self.ctx).await - } else { + if *self.username.lock().await == "config" { menu(stdio.clone(), stdio, false, self.ctx).await + } else { + serial(stdio.clone(), stdio, self.ctx).await } }; @@ -292,12 +335,11 @@ async fn net_task(stack: &'static Stack>) -> ! { } #[embassy_executor::task] -async fn usb_serial_task(usb: embassy_rp::peripherals::USB, +async fn usb_serial_task( + usb: embassy_rp::peripherals::USB, irq: embassy_rp::interrupt::USBCTRL_IRQ, global: &'static GlobalState, - ) -> ! { - +) -> ! { usbserial::usb_serial(usb, irq, global).await; todo!("shoudln't exit"); } - diff --git a/embassy/demos/picow/src/picowmenu.rs b/embassy/demos/picow/src/picowmenu.rs index 1856c46..304497b 100644 --- a/embassy/demos/picow/src/picowmenu.rs +++ b/embassy/demos/picow/src/picowmenu.rs @@ -1,17 +1,26 @@ use core::fmt::Write; use core::future::{poll_fn, Future}; -use core::sync::atomic::Ordering::{Relaxed, SeqCst}; use core::ops::DerefMut; +use core::sync::atomic::Ordering::{Relaxed, SeqCst}; +use embedded_io::asynch; +use embedded_io::asynch::Write as _; use embassy_sync::waitqueue::MultiWakerRegistration; +use heapless::{String, Vec}; + use crate::demo_common; use crate::GlobalState; use demo_common::{BufOutput, SSHConfig}; use demo_common::menu::*; +use sunset::packets::Ed25519PubKey; + +// arbitrary in bytes, for sizing buffers +const MAX_PW_LEN: usize = 50; + pub(crate) struct MenuCtx { pub out: BufOutput, pub state: &'static GlobalState, @@ -19,16 +28,27 @@ pub(crate) struct MenuCtx { // flags to be handled by the calling async loop pub switch_usb1: bool, pub need_save: bool, + + pub logout: bool, + pub reset: bool, } impl MenuCtx { pub fn new(state: &'static GlobalState) -> Self { - Self { state, out: Default::default(), switch_usb1: false, need_save: false } + Self { + state, + out: Default::default(), + switch_usb1: false, + need_save: false, + logout: false, + reset: false, + } } fn with_config(&mut self, f: F) -> bool - where F: FnOnce(&mut SSHConfig, &mut BufOutput) - { + where + F: FnOnce(&mut SSHConfig, &mut BufOutput), + { let mut c = match self.state.config.try_lock() { Ok(c) => c, Err(e) => { @@ -50,6 +70,11 @@ impl core::fmt::Write for MenuCtx { pub(crate) const SETUP_MENU: Menu = Menu { label: "setup", items: &[ + &Item { + command: "logout", + help: None, + item_type: ItemType::Callback { function: do_logout, parameters: &[] }, + }, &AUTH_ITEM, &GPIO_ITEM, &SERIAL_ITEM, @@ -76,7 +101,7 @@ pub(crate) const SETUP_MENU: Menu = Menu { help: None, }, ], - entry: Some(enter_top), + entry: None, exit: None, }; @@ -93,6 +118,17 @@ const AUTH_ITEM: Item = Item { }, help: None, }, + &Item { + command: "console-noauth", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "yesno", + help: Some("Set yes for SSH to serial with no auth. Take care!"), + }], + function: do_console_noauth, + }, + help: None, + }, &Item { command: "key", item_type: ItemType::Callback { @@ -115,7 +151,18 @@ const AUTH_ITEM: Item = Item { // "An OpenSSH style ed25519 key, eg // key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AA...", // ), - function: do_auth_key, + function: do_key, + }, + help: None, + }, + &Item { + command: "clear-key", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "slot", + help: None, + }], + function: do_clear_key, }, help: None, }, @@ -126,7 +173,71 @@ const AUTH_ITEM: Item = Item { parameter_name: "pw", help: None, }], - function: do_auth_pw, + function: do_console_pw, + }, + help: None, + }, + &Item { + command: "disable-password", + item_type: ItemType::Callback { + parameters: &[], + function: do_console_clear_pw, + }, + help: None, + }, + &Item { + command: "admin-key", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "slot", help: None }, + Parameter::Mandatory { + parameter_name: "ssh-ed25519", + help: None, + }, + Parameter::Mandatory { + parameter_name: "base64", + help: None, + }, + Parameter::Optional { + parameter_name: "comment", + help: None, + }, + ], + // help: Some( + // "An OpenSSH style ed25519 key, eg + // key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AA...", + // ), + function: do_admin_key, + }, + help: None, + }, + &Item { + command: "clear-admin-key", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "slot", + help: None, + }], + function: do_admin_clear_key, + }, + help: None, + }, + &Item { + command: "admin-password", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "pw", + help: None, + }], + function: do_admin_pw, + }, + help: Some("Password for serial or config@. 'None' to clear"), + }, + &Item { + command: "clear-admin-password", + item_type: ItemType::Callback { + parameters: &[], + function: do_admin_clear_pw, }, help: None, }, @@ -146,8 +257,14 @@ const WIFI_ITEM: Item = Item { command: "wpa2", item_type: ItemType::Callback { parameters: &[ - Parameter::Mandatory { parameter_name: "net", help: Some("ssid") }, - Parameter::Mandatory { parameter_name: "password", help: None }, + Parameter::Mandatory { + parameter_name: "net", + help: Some("ssid"), + }, + Parameter::Mandatory { + parameter_name: "password", + help: None, + }, ], function: do_wifi_wpa2, }, @@ -156,9 +273,10 @@ const WIFI_ITEM: Item = Item { &Item { command: "open", item_type: ItemType::Callback { - parameters: &[ - Parameter::Mandatory { parameter_name: "net", help: Some("ssid") }, - ], + parameters: &[Parameter::Mandatory { + parameter_name: "net", + help: Some("ssid"), + }], function: do_wifi_open, }, help: None, @@ -204,45 +322,149 @@ const GPIO_ITEM: Item = Item { help: Some("GPIO, todo"), }; - const SERIAL_ITEM: Item = Item { command: "serial", item_type: ItemType::Menu(&Menu { label: "serial", - items: &[ - &Item { - command: "usb0", - item_type: ItemType::Callback { - parameters: &[], - function: do_usb1, - }, - help: Some("Connect to if00 serial port. Disconnect to exit."), - }, - ], + items: &[&Item { + command: "usb0", + item_type: ItemType::Callback { parameters: &[], function: do_usb1 }, + help: Some("Connect to if00 serial port. Disconnect to exit."), + }], entry: None, exit: None, }), help: Some("Passwords and Keys."), }; -fn enter_top(context: &mut MenuCtx) { - writeln!(context, "In setup menu").unwrap(); -} - fn enter_auth(context: &mut MenuCtx) { writeln!(context, "In auth menu").unwrap(); } +fn endis(v: bool) -> &'static str { + if v { + "enabled" + } else { + "disabled" + } +} + +fn prkey(context: &mut dyn Write, name: &str, k: &Option) { + if let Some(k) = k { + writeln!(context, "{} ed25519 todo", name); + } else { + writeln!(context, "{} disabled", name); + } +} + fn do_auth_show(_item: &Item, _args: &[&str], context: &mut MenuCtx) { - writeln!(context, "auth key"); + context.with_config(|c, out| { + write!(out, "Console password "); + if c.console_noauth { + writeln!(out, "not required"); + } else { + writeln!(out, "{}", endis(c.console_pw.is_some())); + } + writeln!(out, "Console password {}", endis(c.console_pw.is_some())); + prkey(out, "Console key1", &c.console_keys[0]); + prkey(out, "Console key2", &c.console_keys[1]); + prkey(out, "Console key3", &c.console_keys[2]); + writeln!(out, "Admin password {}", endis(c.admin_pw.is_some())); + prkey(out, "Admin key1", &c.admin_keys[0]); + prkey(out, "Admin key2", &c.admin_keys[1]); + prkey(out, "Admin key3", &c.admin_keys[2]); + }); } -fn do_auth_key(_item: &Item, _args: &[&str], context: &mut MenuCtx) { - writeln!(context, "auth key"); +fn do_key(_item: &Item, args: &[&str], context: &mut MenuCtx) { + let slot: usize = match args[0].parse() { + Err(e) => { + writeln!(context, "Bad slot"); + return; + } + Ok(s) => s, + }; + if slot == 0 || slot > demo_common::config::KEY_SLOTS { + writeln!(context, "Bad slot"); + return; + } + context.need_save = true; + + writeln!(context, "todo openssh key parsing"); } -fn do_auth_pw(_item: &Item, _args: &[&str], context: &mut MenuCtx) { - writeln!(context, "this is auth pw"); +fn do_clear_key(_item: &Item, args: &[&str], context: &mut MenuCtx) { + writeln!(context, "todo"); + context.need_save = true; +} + +fn do_console_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { + let pw = args[0]; + if pw.as_bytes().len() > MAX_PW_LEN { + writeln!(context, "Too long"); + return; + } + context.with_config(|c, out| { + match c.set_console_pw(Some(pw)) { + Ok(()) => writeln!(out, "Set console password"), + Err(e) => writeln!(out, "Failed setting, {}", e), + }; + }); + context.need_save = true; +} + +// TODO: this is a bit hazardous with the takepipe kickoff mechanism +fn do_console_noauth(_item: &Item, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + c.console_noauth = args[0] == "yes"; + let _ = writeln!(out, "Set console noauth {}", if c.console_noauth { + "yes" + } else { + "no" + }); + }); + context.need_save = true; +} + +fn do_admin_key(_item: &Item, args: &[&str], context: &mut MenuCtx) { + writeln!(context, "todo"); + context.need_save = true; +} + +fn do_admin_clear_key(_item: &Item, args: &[&str], context: &mut MenuCtx) { + writeln!(context, "todo"); + context.need_save = true; +} + +fn do_console_clear_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + let _ = c.set_console_pw(None); + writeln!(out, "Disabled console password"); + }); + context.need_save = true; +} + +fn do_admin_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { + let pw = args[0]; + if pw.as_bytes().len() > MAX_PW_LEN { + writeln!(context, "Too long"); + return; + } + context.with_config(|c, out| { + match c.set_admin_pw(Some(pw)) { + Ok(()) => writeln!(out, "Set admin password"), + Err(e) => writeln!(out, "Failed setting, {}", e), + }; + }); + context.need_save = true; +} + +fn do_admin_clear_pw(_item: &Item, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + let _ = c.set_admin_pw(None); + writeln!(out, "Disabled admin password"); + }); + context.need_save = true; } fn do_gpio_show(_item: &Item, _args: &[&str], context: &mut MenuCtx) { @@ -251,13 +473,21 @@ fn do_gpio_show(_item: &Item, _args: &[&str], context: &mut MenuCtx) { fn do_gpio_set(_item: &Item, _args: &[&str], context: &mut MenuCtx) {} -fn do_erase_config(_item: &Item, args: &[&str], context: &mut MenuCtx) { +fn do_erase_config(_item: &Item, args: &[&str], context: &mut MenuCtx) {} + +fn do_logout(_item: &Item, args: &[&str], context: &mut MenuCtx) { + context.logout = true; } -fn do_reset(_item: &Item, args: &[&str], context: &mut MenuCtx) {} +fn do_reset(_item: &Item, args: &[&str], context: &mut MenuCtx) { + context.reset = true; +} fn do_about(_item: &Item, _args: &[&str], context: &mut MenuCtx) { - let _ = writeln!(context, "Sunset SSH, USB serial\nMatt Johnston \n"); + let _ = writeln!( + context, + "Sunset SSH, USB serial\nMatt Johnston \n" + ); } fn do_usb1(_item: &Item, _args: &[&str], context: &mut MenuCtx) { @@ -307,3 +537,24 @@ fn do_wifi_open(_item: &Item, args: &[&str], context: &mut MenuCtx) { context.need_save = true; wifi_entry(context); } + +// Returns an error on EOF etc. +pub(crate) async fn request_pw( + tx: &mut impl asynch::Write, + rx: &mut impl asynch::Read, +) -> Result, ()> { + tx.write_all(b"\r\nEnter Password: ").await.map_err(|_| ())?; + let mut pw = Vec::::new(); + loop { + let mut c = [0u8]; + rx.read_exact(&mut c).await.map_err(|_| ())?; + let c = c[0]; + if c == b'\r' || c == b'\n' { + break; + } + pw.push(c).map_err(|_| ())?; + } + + let pw = core::str::from_utf8(&pw).map_err(|_| ())?; + return Ok(pw.into()); +} diff --git a/embassy/demos/picow/src/takepipe.rs b/embassy/demos/picow/src/takepipe.rs index 98f8f82..c2bd835 100644 --- a/embassy/demos/picow/src/takepipe.rs +++ b/embassy/demos/picow/src/takepipe.rs @@ -101,6 +101,10 @@ impl<'a> TakeBase<'a> { (r, w) } + pub fn is_in_use(&self) -> bool { + self.shared_read.try_lock().is_err() + } + pub fn split(&'a self) -> (TakeBaseRead<'a>, TakeBaseWrite<'a>) { let r = TakeBaseRead { pipe: self.pipe, diff --git a/embassy/demos/picow/src/usbserial.rs b/embassy/demos/picow/src/usbserial.rs index 5780265..02ee0c9 100644 --- a/embassy/demos/picow/src/usbserial.rs +++ b/embassy/demos/picow/src/usbserial.rs @@ -20,6 +20,7 @@ use sunset::*; use sunset_embassy::*; use crate::*; +use picowmenu::request_pw; pub(crate) async fn usb_serial( usb: embassy_rp::peripherals::USB, @@ -27,8 +28,6 @@ pub(crate) async fn usb_serial( global: &'static GlobalState, ) { - info!("usb_serial top"); - let driver = embassy_rp::usb::Driver::new(usb, irq); let mut config = embassy_usb::Config::new(0xf055, 0x6053); @@ -78,14 +77,15 @@ pub(crate) async fn usb_serial( // Run the USB device. let usb_fut = usb.run(); + // console via SSH on if00 let io0 = async { let (mut chan_rx, mut chan_tx) = global.usb_pipe.split(); let chan_rx = &mut chan_rx; let chan_tx = &mut chan_tx; loop { - info!("usb waiting"); + info!("USB waiting"); cdc0_rx.wait_connection().await; - info!("Connected"); + info!("USB connected"); let mut cdc0_tx = CDCWrite::new(&mut cdc0_tx); let mut cdc0_rx = CDCRead::new(&mut cdc0_rx); @@ -93,25 +93,44 @@ pub(crate) async fn usb_serial( let io_rx = io_copy::<64, _, _>(chan_rx, &mut cdc0_tx); let _ = join(io_rx, io_tx).await; - info!("Disconnected"); + info!("USB disconnected"); } }; + // Admin menu on if02 let setup = async { - loop { - info!("usb waiting"); + 'usb: loop { cdc2_rx.wait_connection().await; - info!("Connected"); - let cdc2_tx = CDCWrite::new(&mut cdc2_tx); - let cdc2_rx = CDCRead::new(&mut cdc2_rx); + let mut cdc2_tx = CDCWrite::new(&mut cdc2_tx); + let mut cdc2_rx = CDCRead::new(&mut cdc2_rx); + + // wait for a keystroke before writing anything. + let mut c = [0u8]; + let _ = cdc2_rx.read_exact(&mut c).await; + + let p = { + let c = global.config.lock().await; + c.admin_pw.clone() + }; + + if let Some(p) = p { + 'pw: loop { + match request_pw(&mut cdc2_tx, &mut cdc2_rx).await { + Ok(pw) => { + if p.check(&pw) { + let _ = cdc2_tx.write_all(b"Good\r\n").await; + break 'pw + } + } + Err(_) => continue 'usb + } + } + } let _ = menu(cdc2_rx, cdc2_tx, true, global).await; - - info!("Disconnected"); } }; - info!("usb join"); join3(usb_fut, io0, setup).await; } @@ -141,13 +160,11 @@ impl<'a, D: Driver<'a>> asynch::Read for CDCRead<'a, '_, D> { .read_packet(ret) .await .map_err(|_| sunset::Error::ChannelEOF)?; - info!("direct read_packet {:?}", &ret[..n]); return Ok(n) } let b = self.fill_buf().await?; let n = ret.len().min(b.len()); - info!("buf read {:?}, rl {} bl {}", &b[..n], ret.len(), b.len()); (&mut ret[..n]).copy_from_slice(&b[..n]); self.consume(n); return Ok(n) @@ -165,12 +182,10 @@ impl<'a, D: Driver<'a>> asynch::BufRead for CDCRead<'a, '_, D> { .read_packet(self.buf.as_mut()) .await .map_err(|_| sunset::Error::ChannelEOF)?; - info!("buf read_packet {:?}", &self.buf[..n]); self.end = n; } debug_assert!(self.end > 0); - info!("fill {}..{}", self.start, self.end); return Ok(&self.buf[self.start..self.end]); } @@ -185,7 +200,6 @@ impl<'a, D: Driver<'a>> asynch::BufRead for CDCRead<'a, '_, D> { self.start = 0; self.end = 0; } - info!("consumed {}, {}..{}", amt, self.start, self.end); } } diff --git a/embassy/demos/picow/src/wifi.rs b/embassy/demos/picow/src/wifi.rs index 9851d3f..ca39d9f 100644 --- a/embassy/demos/picow/src/wifi.rs +++ b/embassy/demos/picow/src/wifi.rs @@ -63,8 +63,10 @@ pub(crate) async fn wifi_stack(spawner: &Spawner, // control.set_power_management(cyw43::PowerManagementMode::None).await; // control.set_power_management(cyw43::PowerManagementMode::Performance).await; + // TODO: this should move out of the critical path, run in the bg. + // just return control before joining. let mut status = Ok(()); - for i in 0..5 { + for i in 0..2 { status = if let Some(ref pw) = wpa_password { info!("wifi net {} wpa2 {}", wifi_net, &pw); control.join_wpa2(&wifi_net, &pw).await @@ -79,10 +81,10 @@ pub(crate) async fn wifi_stack(spawner: &Spawner, } } - if let Err(e) = status { - // wait forever - let () = futures::future::pending().await; - } + // if let Err(e) = status { + // // wait forever + // let () = futures::future::pending().await; + // } let config = embassy_net::Config::Dhcp(Default::default()); diff --git a/embassy/demos/std/Cargo.lock b/embassy/demos/std/Cargo.lock index 18d98ca..a5e023f 100644 --- a/embassy/demos/std/Cargo.lock +++ b/embassy/demos/std/Cargo.lock @@ -127,6 +127,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bcrypt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a" +dependencies = [ + "base64", + "blowfish", + "getrandom", + "subtle", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -142,6 +160,16 @@ dependencies = [ "generic-array 0.14.6", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -1050,6 +1078,7 @@ dependencies = [ name = "sunset-demo-embassy-common" version = "0.1.0" dependencies = [ + "bcrypt", "embassy-futures", "embassy-net", "embassy-net-driver", @@ -1057,7 +1086,10 @@ dependencies = [ "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "embedded-io", "heapless", + "hmac", "log", + "pretty-hex", + "sha2", "sunset", "sunset-embassy", "sunset-sshwire-derive", diff --git a/embassy/demos/std/src/main.rs b/embassy/demos/std/src/main.rs index d8869a0..ccefa82 100644 --- a/embassy/demos/std/src/main.rs +++ b/embassy/demos/std/src/main.rs @@ -100,7 +100,7 @@ impl Shell for DemoShell { let mut menu_buf = [0u8; 150]; let menu_out = demo_menu::BufOutput::default(); - let mut menu = MenuRunner::new(&setupmenu::SETUP_MENU, &mut menu_buf, menu_out); + let mut menu = MenuRunner::new(&setupmenu::SETUP_MENU, &mut menu_buf, true, menu_out); // bodge for c in "help\r\n".bytes() { diff --git a/src/behaviour.rs b/src/behaviour.rs index 471e7b5..c9a48ff 100644 --- a/src/behaviour.rs +++ b/src/behaviour.rs @@ -238,7 +238,7 @@ pub trait ServBehaviour { /// /// Implementations may need to take care to avoid leaking user existence /// based on timing. - fn auth_password(&mut self, username: TextString, password: TextString) -> bool { + async fn auth_password(&mut self, username: TextString<'_>, password: TextString<'_>) -> bool { false } @@ -249,7 +249,7 @@ pub trait ServBehaviour { /// Implementations may need to take care to avoid leaking user existence /// based on timing. #[allow(unused)] - fn auth_pubkey(&mut self, username: TextString, pubkey: &PubKey) -> bool { + async fn auth_pubkey(&mut self, username: TextString<'_>, pubkey: &PubKey<'_>) -> bool { false } diff --git a/src/kex.rs b/src/kex.rs index f251569..0918b84 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -791,8 +791,8 @@ mod tests { } impl<'a> ServBehaviour for TestServBehaviour<'a> { - fn hostkeys(&mut self) -> BhResult<&[&'a SignKey]> { - Ok(self.keys.as_slice()) + fn hostkeys(&mut self) -> BhResult> { + Ok(heapless::Vec::from_slice(self.keys.as_slice()).unwrap()) } fn have_auth_pubkey(&self, _username: TextString) -> bool { diff --git a/src/servauth.rs b/src/servauth.rs index 5b701ee..81326f6 100644 --- a/src/servauth.rs +++ b/src/servauth.rs @@ -36,6 +36,9 @@ impl ServAuth { FailNoReply, } + // TODO: what to do they've already authed? we have to be careful in case + // behaviours don't handle it well. + let username = p.username.clone(); let inner = async { @@ -45,18 +48,28 @@ impl ServAuth { } let success = match p.method { - AuthMethod::Password(m) => b.auth_password(p.username, m.password), + AuthMethod::Password(m) => { + if b.have_auth_password(p.username) { + b.auth_password(p.username, m.password).await + } else { + false + } + } AuthMethod::PubKey(ref m) => { - let allowed_key = b.auth_pubkey(p.username, &m.pubkey.0); - if allowed_key { - if m.sig.is_some() { - self.verify_sig(&mut p, sess_id) + if b.have_auth_pubkey(p.username) { + let allowed_key = b.auth_pubkey(p.username, &m.pubkey.0).await; + if allowed_key { + if m.sig.is_some() { + self.verify_sig(&mut p, sess_id) + } else { + s.send(Userauth60::PkOk(UserauthPkOk { + algo: m.sig_algo, + key: m.pubkey.clone(), + }))?; + return Ok(AuthResp::FailNoReply); + } } else { - s.send(Userauth60::PkOk(UserauthPkOk { - algo: m.sig_algo, - key: m.pubkey.clone(), - }))?; - return Ok(AuthResp::FailNoReply); + false } } else { false diff --git a/src/sign.rs b/src/sign.rs index 5357ef3..f2c1ba3 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -168,7 +168,7 @@ pub enum KeyType { /// /// This may hold the private key part locally /// or potentially send the signing requests to an SSH agent or other entity. -#[derive(ZeroizeOnDrop, Clone)] +#[derive(ZeroizeOnDrop, Clone, PartialEq)] pub enum SignKey { // 32 byte seed value is the private key Ed25519([u8; 32]), diff --git a/src/sshwire.rs b/src/sshwire.rs index 1d4b60e..8e9193c 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -11,7 +11,7 @@ use { log::{debug, error, info, log, trace, warn}, }; -use core::str; +use core::str::FromStr; use core::convert::AsRef; use core::fmt::{self,Debug,Display}; use digest::Output; @@ -302,7 +302,12 @@ impl<'de> SSHDecode<'de> for BinString<'de> { let len = u32::dec(s)? as usize; Ok(BinString(s.take(len)?)) } +} +impl SSHEncode for heapless::String { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.as_str().enc(s) + } } /// A text string that may be presented to a user or used @@ -578,6 +583,14 @@ impl<'de, const N: usize> SSHDecode<'de> for [u8; N] { } } +impl<'de, const N: usize> SSHDecode<'de> for heapless::String { + fn dec(s: &mut S) -> WireResult + where S: SSHSource<'de> { + heapless::String::from_str(SSHDecode::dec(s)?) + .map_err(|_| WireError::NoRoom) + } +} + /// Like `digest::DynDigest` but simpler. /// /// Doesn't have any optional methods that depend on `alloc`. diff --git a/testing/ci.sh b/testing/ci.sh index 192f4a2..28a8a01 100755 --- a/testing/ci.sh +++ b/testing/ci.sh @@ -52,6 +52,11 @@ cd embassy/demos/std cargo build ) +( +cd embassy/demos/common +cargo test +) + ( cd embassy/demos/picow cargo build --release