From 8f362e7d1ea7bc0ab3c13827bc1b534628da6707 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 8 Sep 2021 16:18:03 -0600 Subject: [PATCH] begin setup flow --- appmgr/Cargo.lock | 80 +++++++++++- appmgr/Cargo.toml | 3 + appmgr/src/bin/embassy-init.rs | 14 +- appmgr/src/middleware/encrypt.rs | 215 +++++++++++++++++++++++++++++++ appmgr/src/middleware/mod.rs | 1 + appmgr/src/util/mod.rs | 8 +- 6 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 appmgr/src/middleware/encrypt.rs diff --git a/appmgr/Cargo.lock b/appmgr/Cargo.lock index c3abe3ffa..9872e0264 100644 --- a/appmgr/Cargo.lock +++ b/appmgr/Cargo.lock @@ -2,6 +2,19 @@ # It is not intended for manual editing. version = 3 +[[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 0.2.1", + "ctr", + "opaque-debug", +] + [[package]] name = "ahash" version = "0.7.4" @@ -122,6 +135,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + [[package]] name = "basic-cookies" version = "0.1.4" @@ -345,6 +364,15 @@ dependencies = [ "serde", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clang-sys" version = "1.2.1" @@ -468,6 +496,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "2.0.0" @@ -589,6 +626,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -759,6 +805,7 @@ dependencies = [ name = "embassy-os" version = "0.3.0-pre.0" dependencies = [ + "aes", "anyhow", "async-trait", "avahi-sys", @@ -777,6 +824,7 @@ dependencies = [ "futures", "git-version", "hex", + "hmac", "http", "hyper-ws-listener", "indexmap", @@ -790,6 +838,7 @@ dependencies = [ "openssh-keys", "openssl", "patch-db", + "pbkdf2", "pin-project", "prettytable-rs", "proptest", @@ -1504,9 +1553,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "libloading" @@ -1835,6 +1884,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "password-hash" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad7268ef9bc463fddde8361d358fbfae1aeeb1fb62eca111cd8c763bf1c5891" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "patch-db" version = "0.1.0" @@ -1875,6 +1935,18 @@ dependencies = [ "syn 1.0.75", ] +[[package]] +name = "pbkdf2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" +dependencies = [ + "crypto-mac", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -2665,7 +2737,7 @@ checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.1.5", "digest", "opaque-debug", ] @@ -2684,7 +2756,7 @@ checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.1.5", "digest", "opaque-debug", ] diff --git a/appmgr/Cargo.toml b/appmgr/Cargo.toml index 3a4e951c6..588678350 100644 --- a/appmgr/Cargo.toml +++ b/appmgr/Cargo.toml @@ -43,6 +43,7 @@ portable = [] production = [] [dependencies] +aes = { version = "0.7.5", features = ["ctr", "armv8"] } anyhow = "1.0.40" async-trait = "0.1.42" avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", version = "0.10.0", branch = "feature/dynamic-linking", features = [ @@ -62,6 +63,7 @@ fd-lock-rs = "0.1.3" futures = "0.3.8" git-version = "0.3.4" hex = "0.4.3" +hmac = "0.11.0" http = "0.2.3" hyper-ws-listener = { git = "https://github.com/Start9Labs/hyper-ws-listener.git", branch = "main" } indexmap = { version = "1.6.2", features = ["serde"] } @@ -75,6 +77,7 @@ nix = "0.22.0" openssh-keys = "0.5.0" openssl = { version = "0.10.30", features = ["vendored"] } patch-db = { version = "*", path = "../../patch-db/patch-db" } +pbkdf2 = "0.9.0" pin-project = "1.0.6" prettytable-rs = "0.8.0" proptest = "1.0.0" diff --git a/appmgr/src/bin/embassy-init.rs b/appmgr/src/bin/embassy-init.rs index 644c10c53..1c00eb0e4 100644 --- a/appmgr/src/bin/embassy-init.rs +++ b/appmgr/src/bin/embassy-init.rs @@ -1,7 +1,10 @@ use std::path::Path; +use std::sync::Arc; use embassy::context::rpc::RpcContextConfig; use embassy::context::{RecoveryContext, SetupContext}; +use embassy::hostname::get_product_key; +use embassy::middleware::encrypt::encrypt; use embassy::util::Invoke; use embassy::{Error, ResultExt}; use http::StatusCode; @@ -14,6 +17,7 @@ fn status_fn(_: i32) -> StatusCode { async fn init(cfg_path: Option<&str>) -> Result<(), Error> { let cfg = RpcContextConfig::load(cfg_path).await?; + embassy::disk::util::mount("LABEL=EMBASSY", "/embassy-os").await?; if tokio::fs::metadata("/embassy-os/disk.guid").await.is_ok() { embassy::disk::main::load( &cfg, @@ -26,11 +30,14 @@ async fn init(cfg_path: Option<&str>) -> Result<(), Error> { log::info!("Loaded Disk"); } else { let ctx = SetupContext::init(cfg_path).await?; + let encrypt = encrypt(Arc::new(get_product_key().await?)); rpc_server!({ command: embassy::setup_api, context: ctx.clone(), status: status_fn, - middleware: [ ] + middleware: [ + encrypt, + ] }) .with_graceful_shutdown({ let mut shutdown = ctx.shutdown.subscribe(); @@ -65,6 +72,11 @@ async fn init(cfg_path: Option<&str>) -> Result<(), Error> { .invoke(embassy::ErrorKind::Filesystem) .await?; embassy::disk::util::bind(&tmp_docker, "/var/lib/docker", false).await?; + Command::new("systemctl") + .arg("restart") + .arg("docker") + .invoke(embassy::ErrorKind::Journald) + .await?; log::info!("Mounted Docker Data"); embassy::ssh::sync_keys_from_db(&secret_store, "/root/.ssh/authorized_keys").await?; log::info!("Synced SSH Keys"); diff --git a/appmgr/src/middleware/encrypt.rs b/appmgr/src/middleware/encrypt.rs new file mode 100644 index 000000000..935a0a53c --- /dev/null +++ b/appmgr/src/middleware/encrypt.rs @@ -0,0 +1,215 @@ +use std::sync::Arc; + +use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher}; +use aes::Aes256Ctr; +use anyhow::anyhow; +use futures::future::BoxFuture; +use futures::{FutureExt, Stream}; +use hmac::Hmac; +use http::{HeaderMap, HeaderValue}; +use pbkdf2::pbkdf2; +use rpc_toolkit::hyper::http::Error as HttpError; +use rpc_toolkit::hyper::{self, Body, Request, Response, StatusCode}; +use rpc_toolkit::rpc_server_helpers::{ + to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4, +}; +use rpc_toolkit::yajrc::RpcMethod; +use rpc_toolkit::Metadata; +use sha2::Sha256; + +use crate::util::Apply; +use crate::Error; + +#[pin_project::pin_project] +pub struct DecryptStream { + key: Arc, + #[pin] + body: Body, + ctr: Vec, + salt: Vec, + aes: Option, +} +impl DecryptStream { + pub fn new(key: Arc, body: Body) -> Self { + DecryptStream { + key, + body, + ctr: Vec::new(), + salt: Vec::new(), + aes: None, + } + } +} +impl Stream for DecryptStream { + type Item = hyper::Result; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + match this.body.poll_next(cx) { + std::task::Poll::Pending => std::task::Poll::Pending, + std::task::Poll::Ready(Some(Ok(bytes))) => std::task::Poll::Ready(Some(Ok({ + let mut buf = &*bytes; + if let Some(aes) = this.aes.as_mut() { + let mut res = buf.to_vec(); + aes.apply_keystream(&mut res); + res.into() + } else { + if this.ctr.len() < 16 && buf.len() > 0 { + let to_read = std::cmp::min(16 - this.ctr.len(), buf.len()); + this.ctr.extend_from_slice(&buf[0..to_read]); + buf = &buf[to_read..]; + } + if this.salt.len() < 16 && buf.len() > 0 { + let to_read = std::cmp::min(16 - this.salt.len(), buf.len()); + this.salt.extend_from_slice(&buf[0..to_read]); + buf = &buf[to_read..]; + } + if this.ctr.len() == 16 && this.salt.len() == 16 { + let mut aeskey = CipherKey::::default(); + pbkdf2::>( + this.key.as_bytes(), + &this.salt, + 100_000, + aeskey.as_mut_slice(), + ); + let ctr = Nonce::::from_slice(&this.ctr); + *this.aes = Some(Aes256Ctr::new(&aeskey, &ctr)); + buf.to_vec().into() + } else { + hyper::body::Bytes::new() + } + } + }))), + std::task::Poll::Ready(a) => std::task::Poll::Ready(a), + } + } +} + +#[pin_project::pin_project] +pub struct EncryptStream { + #[pin] + body: Body, + aes: Aes256Ctr, + prefix: Option<[u8; 32]>, +} +impl EncryptStream { + pub fn new(key: &str, body: Body) -> Self { + let prefix: [u8; 32] = rand::random(); + let mut aeskey = CipherKey::::default(); + pbkdf2::>( + key.as_bytes(), + &prefix[16..], + 100_000, + aeskey.as_mut_slice(), + ); + let ctr = Nonce::::from_slice(&prefix[..16]); + let aes = Aes256Ctr::new(&aeskey, &ctr); + EncryptStream { + body, + aes, + prefix: Some(prefix), + } + } +} +impl Stream for EncryptStream { + type Item = hyper::Result; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + if let Some(prefix) = this.prefix.take() { + std::task::Poll::Ready(Some(Ok(prefix.to_vec().into()))) + } else { + match this.body.poll_next(cx) { + std::task::Poll::Pending => std::task::Poll::Pending, + std::task::Poll::Ready(Some(Ok(bytes))) => std::task::Poll::Ready(Some(Ok({ + let mut res = bytes.to_vec(); + this.aes.apply_keystream(&mut res); + res.into() + }))), + std::task::Poll::Ready(a) => std::task::Poll::Ready(a), + } + } + } +} + +fn encrypted(headers: &HeaderMap) -> bool { + headers + .get("Content-Encoding") + .and_then(|h| { + h.to_str() + .ok()? + .split(",") + .any(|s| s == "aesctr256") + .apply(Some) + }) + .unwrap_or_default() +} + +pub fn encrypt(key: Arc) -> DynMiddleware { + Box::new( + move |req: &mut Request, + metadata: M| + -> BoxFuture>, HttpError>> { + let key = key.clone(); + async move { + let encrypted = encrypted(req.headers()); + if encrypted { + let body = std::mem::take(req.body_mut()); + *req.body_mut() = Body::wrap_stream(DecryptStream::new(key.clone(), body)); + }; + let res: DynMiddlewareStage2 = Box::new(move |req, rpc_req| { + async move { + if !encrypted + && metadata + .get(&rpc_req.method.as_str(), "encrypted") + .unwrap_or_default() + { + let (res_parts, _) = Response::new(()).into_parts(); + Ok(Err(to_response( + &req.headers, + res_parts, + Err(Error::new( + anyhow!("Must be encrypted"), + crate::ErrorKind::Authorization, + ) + .into()), + |_| StatusCode::OK, + )?)) + } else { + let res: DynMiddlewareStage3 = Box::new(move |_, _| { + async move { + let res: DynMiddlewareStage4 = Box::new(move |res| { + async move { + if encrypted { + res.headers_mut().insert( + "Content-Encoding", + HeaderValue::from_static("aesctr256"), + ); + let body = std::mem::take(res.body_mut()); + *res.body_mut() = Body::wrap_stream( + EncryptStream::new(&*key, body), + ); + } + Ok(()) + } + .boxed() + }); + Ok(Ok(res)) + } + .boxed() + }); + Ok(Ok(res)) + } + } + .boxed() + }); + Ok(Ok(res)) + } + .boxed() + }, + ) +} diff --git a/appmgr/src/middleware/mod.rs b/appmgr/src/middleware/mod.rs index 7b5029640..3a21fd142 100644 --- a/appmgr/src/middleware/mod.rs +++ b/appmgr/src/middleware/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod cors; +pub mod encrypt; diff --git a/appmgr/src/util/mod.rs b/appmgr/src/util/mod.rs index 47802451f..08306601d 100644 --- a/appmgr/src/util/mod.rs +++ b/appmgr/src/util/mod.rs @@ -347,14 +347,14 @@ pub async fn daemon Fut, Fut: Future + Send + 'static mut shutdown: tokio::sync::broadcast::Receiver>, ) -> Result<(), anyhow::Error> { loop { - match tokio::spawn(f()).await { - Err(e) if e.is_panic() => return Err(anyhow!("daemon panicked!")), - _ => (), - } tokio::select! { _ = shutdown.recv() => return Ok(()), _ = tokio::time::sleep(cooldown) => (), } + match tokio::spawn(f()).await { + Err(e) if e.is_panic() => return Err(anyhow!("daemon panicked!")), + _ => (), + } } }