From b382920c811edec5bd13d8cc8cca3e294fc012ab Mon Sep 17 00:00:00 2001 From: zonyitoo Date: Sun, 26 Feb 2023 12:09:58 +0800 Subject: [PATCH] relax requirement of base64 padding in configuration - allow non-canonical base64 - unify base64 engine in shadowsocks crate - ref shadowsocks/shadowsocks-android#2995 - ref https://github.com/shadowsocks/shadowsocks-android/commit/243cafd41d0712da1f5693067f1f9b0fc6408645 --- Cargo.lock | 7 +- Cargo.toml | 2 +- crates/shadowsocks-service/Cargo.toml | 5 +- crates/shadowsocks-service/src/config.rs | 9 +-- .../shadowsocks-service/src/local/tun/tcp.rs | 2 +- .../shadowsocks-service/src/manager/server.rs | 27 ++----- crates/shadowsocks/Cargo.toml | 2 +- crates/shadowsocks/src/config.rs | 75 ++++++++++++++----- 8 files changed, 74 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0fc9ea8..f6d11d00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2201,7 +2201,7 @@ dependencies = [ [[package]] name = "shadowsocks" -version = "1.15.2" +version = "1.15.3" dependencies = [ "aes", "arc-swap", @@ -2262,7 +2262,7 @@ dependencies = [ [[package]] name = "shadowsocks-rust" -version = "1.15.2" +version = "1.15.3" dependencies = [ "base64", "build-time", @@ -2298,11 +2298,10 @@ dependencies = [ [[package]] name = "shadowsocks-service" -version = "1.15.2" +version = "1.15.3" dependencies = [ "arc-swap", "async-trait", - "base64", "byte_string", "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 5ef4fcdb..2ee9ca13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks-rust" -version = "1.15.2" +version = "1.15.3" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" diff --git a/crates/shadowsocks-service/Cargo.toml b/crates/shadowsocks-service/Cargo.toml index 268c444d..fc6efbc4 100644 --- a/crates/shadowsocks-service/Cargo.toml +++ b/crates/shadowsocks-service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks-service" -version = "1.15.2" +version = "1.15.3" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" @@ -120,9 +120,8 @@ smoltcp = { version = "0.9", optional = true, default-features = false, features serde = { version = "1.0", features = ["derive"] } json5 = "0.4" -base64 = "0.21" -shadowsocks = { version = "1.15.0", path = "../shadowsocks", default-features = false } +shadowsocks = { version = "1.15.3", path = "../shadowsocks", default-features = false } # Just for the ioctl call macro [target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] diff --git a/crates/shadowsocks-service/src/config.rs b/crates/shadowsocks-service/src/config.rs index 11236b79..79c1dd4e 100644 --- a/crates/shadowsocks-service/src/config.rs +++ b/crates/shadowsocks-service/src/config.rs @@ -41,7 +41,6 @@ //! //! These defined server will be used with a load balancing algorithm. -use base64::Engine as _; use std::{ borrow::Cow, convert::{From, Infallible}, @@ -1725,8 +1724,8 @@ impl Config { let mut user_manager = ServerUserManager::new(); for user in users { - let key = match base64::engine::general_purpose::STANDARD.decode(&user.password) { - Ok(k) => k, + let user = match ServerUser::with_encoded_key(user.name, &user.password) { + Ok(u) => u, Err(..) => { let err = Error::new( ErrorKind::Malformed, @@ -1737,7 +1736,7 @@ impl Config { } }; - user_manager.add_user(ServerUser::new(user.name, key)); + user_manager.add_user(user); } nsvr.set_user_manager(user_manager); @@ -2498,7 +2497,7 @@ impl fmt::Display for Config { for u in m.users_iter() { vu.push(SSServerUserConfig { name: u.name().to_owned(), - password: base64::engine::general_purpose::STANDARD.encode(u.key()), + password: u.encoded_key(), }); } vu diff --git a/crates/shadowsocks-service/src/local/tun/tcp.rs b/crates/shadowsocks-service/src/local/tun/tcp.rs index 77366f9a..37192fdc 100644 --- a/crates/shadowsocks-service/src/local/tun/tcp.rs +++ b/crates/shadowsocks-service/src/local/tun/tcp.rs @@ -18,7 +18,7 @@ use shadowsocks::{net::TcpSocketOpts, relay::socks5::Address}; use smoltcp::{ iface::{Config as InterfaceConfig, Interface, SocketHandle, SocketSet}, phy::{DeviceCapabilities, Medium}, - socket::{tcp::Socket as TcpSocket, tcp::SocketBuffer as TcpSocketBuffer, tcp::State as TcpState}, + socket::tcp::{Socket as TcpSocket, SocketBuffer as TcpSocketBuffer, State as TcpState}, storage::RingBuffer, time::{Duration as SmolDuration, Instant as SmolInstant}, wire::{IpAddress, IpCidr, Ipv4Address, Ipv6Address, TcpPacket}, diff --git a/crates/shadowsocks-service/src/manager/server.rs b/crates/shadowsocks-service/src/manager/server.rs index ad0ac69f..9c77d27f 100644 --- a/crates/shadowsocks-service/src/manager/server.rs +++ b/crates/shadowsocks-service/src/manager/server.rs @@ -1,12 +1,9 @@ //! Shadowsocks Manager server -use base64::Engine as _; - #[cfg(unix)] use std::path::PathBuf; use std::{collections::HashMap, io, net::SocketAddr, sync::Arc, time::Duration}; -use base64::engine::general_purpose::STANDARD; use log::{error, info, trace}; use shadowsocks::{ config::{Mode, ServerConfig, ServerType, ServerUser, ServerUserManager}, @@ -14,22 +11,12 @@ use shadowsocks::{ crypto::CipherKind, dns_resolver::DnsResolver, manager::protocol::{ - self, - AddRequest, - AddResponse, - ErrorResponse, - ListResponse, - ManagerRequest, - PingResponse, - RemoveRequest, - RemoveResponse, - ServerUserConfig, - StatRequest, + self, AddRequest, AddResponse, ErrorResponse, ListResponse, ManagerRequest, PingResponse, RemoveRequest, + RemoveResponse, ServerUserConfig, StatRequest, }, net::{AcceptOpts, ConnectOpts}, plugin::PluginConfig, - ManagerListener, - ServerAddr, + ManagerListener, ServerAddr, }; use tokio::{sync::Mutex, task::JoinHandle}; @@ -472,8 +459,8 @@ impl Manager { let mut user_manager = ServerUserManager::new(); for user in users.iter() { - let key = match STANDARD.decode(&user.password) { - Ok(key) => key, + let user = match ServerUser::with_encoded_key(&user.name, &user.password) { + Ok(u) => u, Err(..) => { error!( "users[].password must be encoded with base64, but found: {}", @@ -487,7 +474,7 @@ impl Manager { } }; - user_manager.add_user(ServerUser::new(&user.name, key)); + user_manager.add_user(user); } svr_cfg.set_user_manager(user_manager); @@ -525,7 +512,7 @@ impl Manager { for user in user_manager.users_iter() { vu.push(ServerUserConfig { name: user.name().to_owned(), - password: STANDARD.encode(user.key()), + password: user.encoded_key(), }); } diff --git a/crates/shadowsocks/Cargo.toml b/crates/shadowsocks/Cargo.toml index a97e269e..36eff9ae 100644 --- a/crates/shadowsocks/Cargo.toml +++ b/crates/shadowsocks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks" -version = "1.15.2" +version = "1.15.3" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" diff --git a/crates/shadowsocks/src/config.rs b/crates/shadowsocks/src/config.rs index 536b46b0..1a6529f7 100644 --- a/crates/shadowsocks/src/config.rs +++ b/crates/shadowsocks/src/config.rs @@ -12,14 +12,12 @@ use std::{ time::Duration, }; -use base64::{ - engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD}, - Engine as _, -}; +use base64::Engine as _; use byte_string::ByteStr; use bytes::Bytes; use cfg_if::cfg_if; use log::error; +use thiserror::Error; use url::{self, Url}; use crate::{ @@ -28,6 +26,27 @@ use crate::{ relay::socks5::Address, }; +const USER_KEY_BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new() + .with_encode_padding(true) + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), +); + +const AEAD2022_PASSWORD_BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new() + .with_encode_padding(true) + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), +); + +const URL_PASSWORD_BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::URL_SAFE, + base64::engine::GeneralPurposeConfig::new() + .with_encode_padding(false) + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), +); + /// Shadowsocks server type #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ServerType { @@ -164,7 +183,7 @@ impl Debug for ServerUser { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("ServerUser") .field("name", &self.name) - .field("key", &STANDARD.encode(&self.key)) + .field("key", &USER_KEY_BASE64_ENGINE.encode(&self.key)) .field("identity_hash", &ByteStr::new(&self.identity_hash)) .finish() } @@ -190,6 +209,15 @@ impl ServerUser { } } + /// Create a user from encoded key + pub fn with_encoded_key(name: N, key: &str) -> Result + where + N: Into, + { + let key = USER_KEY_BASE64_ENGINE.decode(key)?; + Ok(ServerUser::new(name, key)) + } + /// Name of the user pub fn name(&self) -> &str { self.name.as_str() @@ -200,6 +228,11 @@ impl ServerUser { self.key.as_ref() } + /// Get Base64 encoded key of user + pub fn encoded_key(&self) -> String { + USER_KEY_BASE64_ENGINE.encode(&self.key) + } + /// User's identity hash /// /// https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2022-2-shadowsocks-2022-extensible-identity-headers.md @@ -215,6 +248,14 @@ impl ServerUser { } } +/// ServerUser related errors +#[derive(Debug, Clone, Error)] +pub enum ServerUserError { + /// Invalid User key encoding + #[error("{0}")] + InvalidKeyEncoding(#[from] base64::DecodeError), +} + /// Server multi-users manager #[derive(Clone, Debug)] pub struct ServerUserManager { @@ -305,7 +346,7 @@ pub struct ServerConfig { fn make_derived_key(method: CipherKind, password: &str, enc_key: &mut [u8]) { if method.is_aead_2022() { // AEAD 2022 password is a base64 form of enc_key - match STANDARD.decode(password) { + match AEAD2022_PASSWORD_BASE64_ENGINE.decode(password) { Ok(v) => { if v.len() != enc_key.len() { panic!( @@ -366,7 +407,7 @@ where make_derived_key(method, upsk, &mut enc_key); for ipsk in split_iter { - match STANDARD.decode(ipsk) { + match USER_KEY_BASE64_ENGINE.decode(ipsk) { Ok(v) => { identity_keys.push(Bytes::from(v)); } @@ -567,7 +608,7 @@ impl ServerConfig { /// ``` pub fn to_qrcode_url(&self) -> String { let param = format!("{}:{}@{}", self.method(), self.password(), self.addr()); - format!("ss://{}", URL_SAFE_NO_PAD.encode(param)) + format!("ss://{}", URL_PASSWORD_BASE64_ENGINE.encode(param)) } /// Get [SIP002](https://github.com/shadowsocks/shadowsocks-org/issues/27) URL @@ -576,13 +617,13 @@ impl ServerConfig { if #[cfg(feature = "aead-cipher-2022")] { let user_info = if !self.method().is_aead_2022() { let user_info = format!("{}:{}", self.method(), self.password()); - URL_SAFE_NO_PAD.encode(&user_info) + URL_PASSWORD_BASE64_ENGINE.encode(&user_info) } else { format!("{}:{}", self.method(), percent_encoding::utf8_percent_encode(self.password(), percent_encoding::NON_ALPHANUMERIC)) }; } else { let mut user_info = format!("{}:{}", self.method(), self.password()); - user_info = URL_SAFE_NO_PAD.encode(&user_info) + user_info = URL_PASSWORD_BASE64_ENGINE.encode(&user_info) } } @@ -632,7 +673,7 @@ impl ServerConfig { None => return Err(UrlParseError::MissingHost), }; - let mut decoded_body = match URL_SAFE_NO_PAD.decode(encoded) { + let mut decoded_body = match URL_PASSWORD_BASE64_ENGINE.decode(encoded) { Ok(b) => match String::from_utf8(b) { Ok(b) => b, Err(..) => return Err(UrlParseError::InvalidServerAddr), @@ -685,15 +726,9 @@ impl ServerConfig { // reborrow to fit AsRef<[u8]> let decoded_user_info: &str = &decoded_user_info; - let base64_config = if decoded_user_info.ends_with('=') { - // Some implementation, like outline, - // or those with Python (base64 in Python will still have '=' padding for URL safe encode) - URL_SAFE - } else { - URL_SAFE_NO_PAD - }; - - let account = match base64_config.decode(decoded_user_info) { + // Some implementation, like outline, + // or those with Python (base64 in Python will still have '=' padding for URL safe encode) + let account = match URL_PASSWORD_BASE64_ENGINE.decode(decoded_user_info) { Ok(account) => match String::from_utf8(account) { Ok(ac) => ac, Err(..) => return Err(UrlParseError::InvalidAuthInfo),