Add experimental encryption support

This commit is contained in:
NGnius (Graham) 2022-07-24 14:45:48 -04:00
parent 36adfa124d
commit f076764fff
10 changed files with 339 additions and 18 deletions

116
Cargo.lock generated
View file

@ -2,6 +2,42 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aead"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array",
]
[[package]]
name = "aes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if 1.0.0",
"cipher",
"cpufeatures",
"opaque-debug",
]
[[package]]
name = "aes-gcm-siv"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"polyval",
"subtle",
"zeroize",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -78,6 +114,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@ -107,6 +152,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -273,6 +327,18 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "http"
version = "0.2.8"
@ -471,6 +537,12 @@ dependencies = [
"libc",
]
[[package]]
name = "obfstr"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2b2cbbfd8defa51ff24450a61d73b3ff3e158484ddd274a883e886e6fbaa78"
[[package]]
name = "once_cell"
version = "1.13.0"
@ -521,6 +593,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "polyval"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@ -686,6 +770,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.98"
@ -912,6 +1002,16 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "url"
version = "2.2.2"
@ -926,9 +1026,11 @@ dependencies = [
[[package]]
name = "usdpl-back"
version = "0.5.3"
version = "0.6.0"
dependencies = [
"bytes",
"hex",
"obfstr",
"tokio",
"usdpl-core",
"warp",
@ -936,9 +1038,11 @@ dependencies = [
[[package]]
name = "usdpl-core"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"aes-gcm-siv",
"base64",
"hex-literal",
]
[[package]]
@ -946,7 +1050,9 @@ name = "usdpl-front"
version = "0.5.0"
dependencies = [
"console_error_panic_hook",
"hex",
"js-sys",
"obfstr",
"usdpl-core",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -1193,3 +1299,9 @@ name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "zeroize"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"

View file

@ -1,6 +1,6 @@
[package]
name = "usdpl-back"
version = "0.5.3"
version = "0.6.0"
edition = "2021"
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
@ -12,11 +12,16 @@ default = ["blocking"]
decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
blocking = ["tokio"] # synchronous API for async functionality, using tokio
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
[dependencies]
usdpl-core = { version = "0.5.0", path = "../usdpl-core" }
usdpl-core = { version = "0.6.0", path = "../usdpl-core"}
# HTTP web framework
warp = { version = "0.3" }
bytes = { version = "1.1" }
tokio = { version = "1.19", features = ["rt", "rt-multi-thread"], optional = true }
# encryption helpers
obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true }

View file

@ -11,10 +11,15 @@ use super::Callable;
type WrappedCallable = Arc<Mutex<Box<dyn Callable>>>; // thread-safe, cloneable Callable
#[cfg(feature = "encrypt")]
const NONCE: [u8; socket::NONCE_SIZE] = [0u8; socket::NONCE_SIZE];
/// Back-end instance for interacting with the front-end
pub struct Instance {
calls: HashMap<String, WrappedCallable>,
port: u16,
#[cfg(feature = "encrypt")]
encryption_key: Vec<u8>,
}
impl Instance {
@ -24,6 +29,8 @@ impl Instance {
Instance {
calls: HashMap::new(),
port: port_usdpl,
#[cfg(feature = "encrypt")]
encryption_key: hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap(),
}
}
@ -100,6 +107,7 @@ impl Instance {
async fn serve_internal(&self) -> Result<(), ()> {
let handlers = self.calls.clone();
//self.calls = HashMap::new();
#[cfg(not(feature = "encrypt"))]
let calls = warp::post()
.and(warp::path!("usdpl" / "call"))
.and(warp::body::content_length_limit(
@ -136,6 +144,47 @@ impl Instance {
)
})
.map(|reply| warp::reply::with_header(reply, "Access-Control-Allow-Origin", "*"));
#[cfg(feature = "encrypt")]
let key = self.encryption_key.clone();
#[cfg(feature = "encrypt")]
let calls = warp::post()
.and(warp::path!("usdpl" / "call"))
.and(warp::body::content_length_limit(
(socket::PACKET_BUFFER_SIZE * 2) as _,
))
.and(warp::body::bytes())
.map(move |data: bytes::Bytes| {
let (packet, _) = match socket::Packet::load_encrypted(&data, &key, &NONCE) {
Ok(x) => x,
Err(_) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body("Failed to load packet".to_string()),
warp::http::StatusCode::from_u16(400).unwrap(),
)
}
};
let mut buffer = Vec::with_capacity(socket::PACKET_BUFFER_SIZE);
buffer.extend(&[0u8; socket::PACKET_BUFFER_SIZE]);
let response = Self::handle_call(packet, &handlers);
let len = match response.dump_encrypted(&mut buffer, &key, &NONCE) {
Ok(x) => x,
Err(_) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body("Failed to dump response packet".to_string()),
warp::http::StatusCode::from_u16(500).unwrap(),
)
}
};
buffer.truncate(len);
let string: String = String::from_utf8(buffer).unwrap().into();
warp::reply::with_status(
warp::http::Response::builder().body(string),
warp::http::StatusCode::from_u16(200).unwrap(),
)
})
.map(|reply| warp::reply::with_header(reply, "Access-Control-Allow-Origin", "*"));
#[cfg(debug_assertions)]
warp::serve(calls).run(([0, 0, 0, 0], self.port)).await;
#[cfg(not(debug_assertions))]

View file

@ -1,6 +1,6 @@
[package]
name = "usdpl-core"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
@ -11,6 +11,11 @@ description = "Universal Steam Deck Plugin Library core"
default = []
decky = []
crankshaft = []
encrypt = ["aes-gcm-siv"]
[dependencies]
base64 = "0.13"
aes-gcm-siv = { version = "0.10", optional = true, default-features = false, features = ["alloc", "aes"] }
[dev-dependencies]
hex-literal = "0.3.4"

View file

@ -2,6 +2,12 @@ use base64::{decode_config_slice, encode_config_slice, Config};
const B64_CONF: Config = Config::new(base64::CharacterSet::Standard, true);
#[cfg(feature = "encrypt")]
const ASSOCIATED_DATA: &[u8] = b"usdpl-core-data";
#[cfg(feature = "encrypt")]
use aes_gcm_siv::aead::{AeadInPlace, NewAead};
/// Errors from Loadable::load
#[derive(Debug)]
pub enum LoadError {
@ -9,6 +15,9 @@ pub enum LoadError {
TooSmallBuffer,
/// Unexpected/corrupted data encountered
InvalidData,
/// Encrypted data cannot be decrypted
#[cfg(feature = "encrypt")]
DecryptionError,
/// Unimplemented
#[cfg(debug_assertions)]
Todo,
@ -19,6 +28,8 @@ impl std::fmt::Display for LoadError {
match self {
Self::TooSmallBuffer => write!(f, "LoadError: TooSmallBuffer"),
Self::InvalidData => write!(f, "LoadError: InvalidData"),
#[cfg(feature = "encrypt")]
Self::DecryptionError => write!(f, "LoadError: DecryptionError"),
#[cfg(debug_assertions)]
Self::Todo => write!(f, "LoadError: TODO!"),
}
@ -38,6 +49,21 @@ pub trait Loadable: Sized {
.map_err(|_| LoadError::InvalidData)?;
Self::load(&buffer2[..len])
}
/// Load data from an encrypted base64-encoded buffer
#[cfg(feature = "encrypt")]
fn load_encrypted(buffer: &[u8], key: &[u8], nonce: &[u8]) -> Result<(Self, usize), LoadError> {
println!("encrypted buffer: {}", String::from_utf8(buffer.to_vec()).unwrap());
let key = aes_gcm_siv::Key::from_slice(key);
let cipher = aes_gcm_siv::Aes256GcmSiv::new(key);
let nonce = aes_gcm_siv::Nonce::from_slice(nonce);
let mut decoded_buf = base64::decode_config(buffer, B64_CONF)
.map_err(|_| LoadError::InvalidData)?;
println!("Decoded buf: {:?}", decoded_buf);
cipher.decrypt_in_place(nonce, ASSOCIATED_DATA, &mut decoded_buf).map_err(|_| LoadError::DecryptionError)?;
println!("Decrypted buf: {:?}", decoded_buf);
Self::load(decoded_buf.as_slice())
}
}
/// Errors from Dumpable::dump
@ -47,6 +73,9 @@ pub enum DumpError {
TooSmallBuffer,
/// Data cannot be dumped
Unsupported,
/// Data cannot be encrypted
#[cfg(feature = "encrypt")]
EncryptionError,
/// Unimplemented
#[cfg(debug_assertions)]
Todo,
@ -57,6 +86,8 @@ impl std::fmt::Display for DumpError {
match self {
Self::TooSmallBuffer => write!(f, "DumpError: TooSmallBuffer"),
Self::Unsupported => write!(f, "DumpError: Unsupported"),
#[cfg(feature = "encrypt")]
Self::EncryptionError => write!(f, "DumpError: EncryptionError"),
#[cfg(debug_assertions)]
Self::Todo => write!(f, "DumpError: TODO!"),
}
@ -77,4 +108,23 @@ pub trait Dumpable {
let len = encode_config_slice(&buffer2[..len], B64_CONF, buffer);
Ok(len)
}
/// Dump data as an encrypted base64-encoded buffer
#[cfg(feature = "encrypt")]
fn dump_encrypted(&self, buffer: &mut Vec<u8>, key: &[u8], nonce: &[u8]) -> Result<usize, DumpError> {
let mut buffer2 = Vec::with_capacity(buffer.capacity());
buffer2.extend_from_slice(buffer.as_slice());
let size = self.dump(&mut buffer2)?;
buffer2.truncate(size);
println!("Buf: {:?}", buffer2);
let key = aes_gcm_siv::Key::from_slice(key);
let cipher = aes_gcm_siv::Aes256GcmSiv::new(key);
let nonce = aes_gcm_siv::Nonce::from_slice(nonce);
cipher.encrypt_in_place(nonce, ASSOCIATED_DATA, &mut buffer2).map_err(|_| DumpError::EncryptionError)?;
println!("Encrypted slice: {:?}", &buffer2);
let size = encode_config_slice(buffer2.as_slice(), B64_CONF, buffer);
let string = String::from_utf8(buffer.as_slice()[..size].to_vec()).unwrap();
println!("Encoded slice: {}", string);
Ok(size)
}
}

View file

@ -11,6 +11,8 @@ pub const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
/// Standard max packet size
pub const PACKET_BUFFER_SIZE: usize = 1024;
/// Encryption nonce size
pub const NONCE_SIZE: usize = 12;
/// Address and port
#[inline]
@ -108,3 +110,38 @@ impl Dumpable for Packet {
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "encrypt")]
#[test]
fn encryption_integration_test() {
let key = hex_literal::hex!("59C4E408F27250B3147E7724511824F1D28ED7BEF43CF7103ACE747F77A2B265");
let nonce = [0u8; NONCE_SIZE];
let packet = Packet::Call(RemoteCall{
id: 42,
function: "test".into(),
parameters: Vec::new(),
});
let mut buffer = Vec::with_capacity(PACKET_BUFFER_SIZE);
buffer.extend_from_slice(&[0u8; PACKET_BUFFER_SIZE]);
let len = packet.dump_encrypted(&mut buffer, &key, &nonce).unwrap();
println!("buffer: {}", String::from_utf8(buffer.as_slice()[..len].to_vec()).unwrap());
let (packet_out, _len) = Packet::load_encrypted(&buffer.as_slice()[..len], &key, &nonce).unwrap();
if let Packet::Call(call_out) = packet_out {
if let Packet::Call(call_in) = packet {
assert_eq!(call_in.id, call_out.id, "Input and output packets do not match");
assert_eq!(call_in.function, call_out.function, "Input and output packets do not match");
assert_eq!(call_in.parameters.len(), call_out.parameters.len(), "Input and output packets do not match");
} else {
panic!("Packet in not a Call");
}
} else {
panic!("Packet out not a Call!");
}
}
}

View file

@ -12,10 +12,11 @@ description = "Universal Steam Deck Plugin Library front-end designed for WASM"
crate-type = ["cdylib", "rlib"]
[features]
default = []
default = ["encrypt"]
decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
debug = ["console_error_panic_hook"]
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
[dependencies]
wasm-bindgen = "0.2"
@ -44,7 +45,10 @@ web-sys = { version = "0.3", features = [
]}
js-sys = { version = "0.3" }
usdpl-core = { version = "0.5.0", path = "../usdpl-core" }
obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true }
usdpl-core = { version = "0.6.0", path = "../usdpl-core" }
[dev-dependencies]
wasm-bindgen-test = { version = "0.3.13" }

View file

@ -6,17 +6,17 @@ $0 [decky|crankshaft|<nothing>]"
exit 0
elif [ "$1" == "decky" ]; then
echo "Building WASM module for decky framework"
wasm-pack build --target web --features decky
RUSTFLAGS="--cfg aes_compact" wasm-pack build --target web --features decky
elif [ "$1" == "crankshaft" ]; then
echo "WARNING: crankshaft support is unimplemented"
wasm-pack build --target web --features crankshaft
RUSTFLAGS="--cfg aes_compact" wasm-pack build --target web --features crankshaft
else
echo "Unsupported plugin framework \`$1\`"
exit 1
fi
else
echo "WARNING: Building for any plugin framework, which may not work for every framework"
wasm-pack build --target web
RUSTFLAGS="--cfg aes_compact" wasm-pack build --target web
fi
python3 ./scripts/generate_embedded_wasm.py

View file

@ -13,18 +13,22 @@ use web_sys::{Request, RequestInit, RequestMode, Response};
use usdpl_core::serdes::{Dumpable, Loadable, Primitive};
use usdpl_core::socket;
pub async fn send_js(packet: socket::Packet, port: u16) -> Result<Vec<Primitive>, JsValue> {
const NONCE: [u8; socket::NONCE_SIZE]= [0u8; socket::NONCE_SIZE];
pub async fn send_js(
packet: socket::Packet,
port: u16,
#[cfg(feature = "encrypt")]
key: Vec<u8>,
) -> Result<Vec<Primitive>, JsValue> {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
let url = format!("http://{}:{}/usdpl/call", socket::HOST_STR, port);
let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE];
let len = packet
.dump_base64(&mut buffer)
.map_err(super::convert::str_to_js)?;
let string: String = String::from_utf8_lossy(&buffer[..len]).into();
let (buffer, len) = dump_to_buffer(packet, #[cfg(feature = "encrypt")] key.as_slice())?;
let string: String = String::from_utf8_lossy(&buffer.as_slice()[..len]).into();
opts.body(Some(&string.into()));
let request = Request::new_with_str_and_init(&url, &opts)?;
@ -39,6 +43,7 @@ pub async fn send_js(packet: socket::Packet, port: u16) -> Result<Vec<Primitive>
let text = JsFuture::from(resp.text()?).await?;
let string: JsString = text.dyn_into()?;
#[cfg(not(feature = "encrypt"))]
match socket::Packet::load_base64(string.as_string().unwrap().as_bytes())
.map_err(super::convert::str_to_js)?
.0
@ -53,4 +58,40 @@ pub async fn send_js(packet: socket::Packet, port: u16) -> Result<Vec<Primitive>
.into())
}
}
#[cfg(feature = "encrypt")]
match socket::Packet::load_encrypted(string.as_string().unwrap().as_bytes(), key.as_slice(), &NONCE)
.map_err(super::convert::str_to_js)?
.0
{
socket::Packet::CallResponse(resp) => Ok(resp.response),
_ => {
//imports::console_warn(&format!("USDPL warning: Got non-call-response message from {}", resp.url()));
Err(format!(
"Expected call response message from {}, got something else",
resp.url()
)
.into())
}
}
}
#[cfg(feature = "encrypt")]
fn dump_to_buffer(packet: socket::Packet, key: &[u8]) -> Result<(Vec<u8>, usize), JsValue> {
let mut buffer = Vec::with_capacity(socket::PACKET_BUFFER_SIZE);
buffer.extend_from_slice(&[0u8; socket::PACKET_BUFFER_SIZE]);
let len = packet
.dump_encrypted(&mut buffer, key, &NONCE)
.map_err(super::convert::str_to_js)?;
Ok((buffer, len))
}
#[cfg(not(feature = "encrypt"))]
fn dump_to_buffer(packet: socket::Packet) -> Result<(Vec<u8>, usize), JsValue> {
let mut buffer = Vec::with_capacity(socket::PACKET_BUFFER_SIZE);
buffer.extend_from_slice(&[0u8; socket::PACKET_BUFFER_SIZE]);
let len = packet
.dump_base64(buffer.as_mut_slice())
.map_err(super::convert::str_to_js)?;
Ok((buffer, len))
}

View file

@ -16,19 +16,30 @@ use usdpl_core::{socket::Packet, RemoteCall};
//const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
//const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337);
static mut CTX: UsdplContext = UsdplContext { port: 31337, id: 1 };
static mut CTX: UsdplContext = UsdplContext { port: 31337, id: 1, key: Vec::new() };
#[cfg(feature = "encrypt")]
fn encryption_key() -> Vec<u8> {
hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap()
}
//#[wasm_bindgen]
#[derive(Debug)]
struct UsdplContext {
port: u16,
id: u64,
#[cfg(feature = "encrypt")]
key: Vec<u8>,
}
fn get_port() -> u16 {
unsafe { CTX.port }
}
fn get_key() -> Vec<u8> {
unsafe { CTX.key.clone() }
}
fn increment_id() -> u64 {
let current_id = unsafe { CTX.id };
unsafe {
@ -49,7 +60,12 @@ pub fn init_usdpl(port: u16) {
console_error_panic_hook::set_once();
//REMOTE_PORT.store(port, std::sync::atomic::Ordering::SeqCst);
unsafe {
CTX = UsdplContext { port: port, id: 1 };
CTX = UsdplContext {
port: port,
id: 1,
#[cfg(feature = "encrypt")]
key: encryption_key(),
};
}
}
@ -84,6 +100,8 @@ pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue {
parameters: params,
}),
port,
#[cfg(feature = "encrypt")]
get_key()
)
.await;
let results = match results {