From 28f9fa35e5c3a2a5bc9f90df87e9e7020bdccabb Mon Sep 17 00:00:00 2001 From: Lucy C <12953208+elvece@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:03:05 -0600 Subject: [PATCH] Fix/encryption (#1811) * change encryption to use pubkey and only encrypt specific fields * adjust script names for convenience * remove unused fn * fix build script name * augment mocks * remove log * fix prod build * feat: backend keys * fix: Using the correct name with the public key * chore: Fix the type for the encrypted * chore: Add some tracing * remove aes-js from package lock file Co-authored-by: BluJ --- Makefile | 4 +- backend/src/bin/embassy-init.rs | 2 - backend/src/context/setup.rs | 34 ++- backend/src/middleware/encrypt.rs | 288 ++++-------------- backend/src/setup.rs | 68 +++-- frontend/lint-staged.config.js | 4 +- frontend/package-lock.json | 22 -- frontend/package.json | 16 +- .../app/modals/cifs-modal/cifs-modal.page.ts | 9 +- .../src/app/pages/home/home.page.ts | 8 +- .../src/app/services/api/api.service.ts | 30 +- .../src/app/services/api/live-api.service.ts | 43 +-- .../src/app/services/api/mock-api.service.ts | 27 +- .../src/app/services/rpc-encrypted.service.ts | 103 ------- .../src/app/services/state.service.ts | 18 +- 15 files changed, 213 insertions(+), 463 deletions(-) delete mode 100644 frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts diff --git a/Makefile b/Makefile index 2e3cfe52a..549e7a623 100644 --- a/Makefile +++ b/Makefile @@ -79,10 +79,10 @@ frontend/dist/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) npm --prefix frontend run build:ui frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) - npm --prefix frontend run build:setup-wizard + npm --prefix frontend run build:setup frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) - npm --prefix frontend run build:diagnostic-ui + npm --prefix frontend run build:dui frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.json jq '.useMocks = false' frontend/config-sample.json > frontend/config.json diff --git a/backend/src/bin/embassy-init.rs b/backend/src/bin/embassy-init.rs index 024ed2261..4e8455cec 100644 --- a/backend/src/bin/embassy-init.rs +++ b/backend/src/bin/embassy-init.rs @@ -48,7 +48,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> { .invoke(embassy::ErrorKind::Nginx) .await?; let ctx = SetupContext::init(cfg_path).await?; - let encrypt = embassy::middleware::encrypt::encrypt(ctx.clone()); tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this CHIME.play().await?; rpc_server!({ @@ -57,7 +56,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> { status: status_fn, middleware: [ cors, - encrypt, ] }) .with_graceful_shutdown({ diff --git a/backend/src/context/setup.rs b/backend/src/context/setup.rs index f6aadabd9..61a3636f8 100644 --- a/backend/src/context/setup.rs +++ b/backend/src/context/setup.rs @@ -3,10 +3,9 @@ use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; +use josekit::jwk::Jwk; use patch_db::json_ptr::JsonPointer; use patch_db::PatchDb; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::Context; use serde::{Deserialize, Serialize}; @@ -70,13 +69,19 @@ pub struct SetupContextSeed { pub datadir: PathBuf, /// Used to encrypt for hidding from snoopers for setups create password /// Set via path - pub current_secret: RwLock>, + pub current_secret: Arc, pub selected_v2_drive: RwLock>, pub cached_product_key: RwLock>>, pub recovery_status: RwLock>>, pub setup_result: RwLock, SetupResult)>>, } +impl AsRef for SetupContextSeed { + fn as_ref(&self) -> &Jwk { + &self.current_secret + } +} + #[derive(Clone)] pub struct SetupContext(Arc); impl SetupContext { @@ -90,7 +95,16 @@ impl SetupContext { bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()), shutdown, datadir, - current_secret: RwLock::new(None), + current_secret: Arc::new( + Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| { + tracing::debug!("{:?}", e); + tracing::error!("Couldn't generate ec key"); + Error::new( + color_eyre::eyre::eyre!("Couldn't generate ec key"), + crate::ErrorKind::Unknown, + ) + })?, + ), selected_v2_drive: RwLock::new(None), cached_product_key: RwLock::new(None), recovery_status: RwLock::new(None), @@ -131,18 +145,6 @@ impl SetupContext { } Ok(secret_store) } - - /// So we assume that there will only be one client that will ask for a secret, - /// And during that time do we upsert to a new key - pub async fn update_secret(&self) -> Result { - let new_secret: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(30) - .map(char::from) - .collect(); - *self.current_secret.write().await = Some(new_secret.clone()); - Ok(new_secret) - } } impl Context for SetupContext { diff --git a/backend/src/middleware/encrypt.rs b/backend/src/middleware/encrypt.rs index 3a3a59849..54064db23 100644 --- a/backend/src/middleware/encrypt.rs +++ b/backend/src/middleware/encrypt.rs @@ -2,23 +2,13 @@ use std::sync::Arc; use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher}; use aes::Aes256Ctr; -use color_eyre::eyre::eyre; -use futures::future::BoxFuture; -use futures::{FutureExt, Stream}; +use futures::Stream; use hmac::Hmac; -use http::{HeaderMap, HeaderValue}; -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 josekit::jwk::Jwk; +use rpc_toolkit::hyper::{self, Body}; +use serde::{Deserialize, Serialize}; use sha2::Sha256; - -use crate::context::SetupContext; -use crate::util::Apply; -use crate::Error; +use tracing::instrument; pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey { let mut aeskey = CipherKey::::default(); @@ -56,219 +46,73 @@ pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec res } -#[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.is_empty() { - 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.is_empty() { - 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 aeskey = pbkdf2(this.key.as_bytes(), &this.salt); - let ctr = Nonce::::from_slice(this.ctr); - let mut aes = Aes256Ctr::new(&aeskey, ctr); - let mut res = buf.to_vec(); - aes.apply_keystream(&mut res); - *this.aes = Some(aes); - res.into() - } else { - hyper::body::Bytes::new() - } - } - }))), - std::task::Poll::Ready(a) => std::task::Poll::Ready(a), - } - } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct EncryptedWire { + encrypted: serde_json::Value, } +impl EncryptedWire { + #[instrument(skip(current_secret))] + pub fn decrypt(self, current_secret: impl AsRef) -> Option { + let current_secret = current_secret.as_ref(); -#[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 aeskey = pbkdf2(key.as_bytes(), &prefix[16..]); - 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), + let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs + .decrypter_from_jwk(current_secret) + { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not setup awk"); + tracing::debug!("{:?}", e); + return None; + } + }; + let encrypted = match serde_json::to_string(&self.encrypted) { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not deserialize"); + tracing::debug!("{:?}", e); + + return None; + } + }; + let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not decrypt"); + tracing::debug!("{:?}", e); + return None; + } + }; + match String::from_utf8(decoded) { + Ok(a) => Some(a), + Err(e) => { + tracing::warn!("Could not decrypt into utf8"); + tracing::debug!("{:?}", e); + return None; } } } } -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(ctx: SetupContext) -> DynMiddleware { - Box::new( - move |req: &mut Request, - metadata: M| - -> BoxFuture>, HttpError>> { - let keysource = ctx.clone(); - async move { - let encrypted = encrypted(req.headers()); - let current_secret: Option = keysource.current_secret.read().await.clone(); - let key = if encrypted { - let key = match current_secret { - Some(s) => s, - None => { - let (res_parts, _) = Response::new(()).into_parts(); - return Ok(Err(to_response( - req.headers(), - res_parts, - Err(Error::new( - eyre!("No Secret has been set"), - crate::ErrorKind::RateLimited, - ) - .into()), - |_| StatusCode::OK, - )?)); - } - }; - let body = std::mem::take(req.body_mut()); - *req.body_mut() = - Body::wrap_stream(DecryptStream::new(Arc::new(key.clone()), body)); - Some(key) - } else { - None - }; - let res: DynMiddlewareStage2 = Box::new(move |req, rpc_req| { - async move { - if !encrypted - && metadata - .get(rpc_req.method.as_str(), "authenticated") - .unwrap_or(true) - { - let (res_parts, _) = Response::new(()).into_parts(); - Ok(Err(to_response( - &req.headers, - res_parts, - Err(Error::new( - eyre!("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 let Some(key) = key { - res.headers_mut().insert( - "Content-Encoding", - HeaderValue::from_static("aesctr256"), - ); - if let Some(len_header) = - res.headers_mut().get_mut("Content-Length") - { - if let Some(len) = len_header - .to_str() - .ok() - .and_then(|l| l.parse::().ok()) - { - *len_header = HeaderValue::from(len + 32); - } - } - let body = std::mem::take(res.body_mut()); - *res.body_mut() = Body::wrap_stream( - EncryptStream::new(key.as_ref(), body), - ); - } - Ok(()) - } - .boxed() - }); - Ok(Ok(res)) - } - .boxed() - }); - Ok(Ok(res)) - } - } - .boxed() - }); - Ok(Ok(res)) - } - .boxed() - }, +/// We created this test by first making the private key, then restoring from this private key for recreatability. +/// After this the frontend then encoded an password, then we are testing that the output that we got (hand coded) +/// will be the shape we want. +#[test] +fn test_gen_awk() { + let private_key: Jwk = serde_json::from_str( + r#"{ + "kty": "EC", + "crv": "P-256", + "d": "3P-MxbUJtEhdGGpBCRFXkUneGgdyz_DGZWfIAGSCHOU", + "x": "yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4", + "y": "8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI" + }"#, ) + .unwrap(); + let encrypted: EncryptedWire = serde_json::from_str(r#"{ + "encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" } + }"#).unwrap(); + assert_eq!( + "testing12345", + &encrypted.decrypt(Arc::new(private_key)).unwrap() + ); } diff --git a/backend/src/setup.rs b/backend/src/setup.rs index 47c30c6c9..7c0d8c64a 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -42,6 +42,7 @@ use crate::hostname::{get_hostname, Hostname}; use crate::id::Id; use crate::init::init; use crate::install::PKG_PUBLIC_DIR; +use crate::middleware::encrypt::EncryptedWire; use crate::net::ssl::SslManager; use crate::s9pk::manifest::PackageId; use crate::sound::BEETHOVEN; @@ -63,7 +64,7 @@ where Ok(password) } -#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_secret))] +#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_pubkey))] pub fn setup() -> Result<(), Error> { Ok(()) } @@ -95,8 +96,20 @@ pub async fn list_disks() -> Result, Error> { pub async fn attach( #[context] ctx: SetupContext, #[arg] guid: Arc, - #[arg(rename = "embassy-password")] password: Option, + #[arg(rename = "embassy-password")] password: Option, ) -> Result { + let password: Option = match password { + Some(a) => match a.decrypt(&*ctx) { + a @ Some(_) => a, + None => { + return Err(Error::new( + color_eyre::eyre::eyre!("Couldn't decode password"), + crate::ErrorKind::Unknown, + )); + } + }, + None => None, + }; let requires_reboot = crate::disk::main::import( &*guid, &ctx.datadir, @@ -202,23 +215,11 @@ pub async fn recovery_status( /// This way the frontend can send a secret, like the password for the setup/ recovory /// without knowing the password over clearnet. We use the public key shared across the network /// since it is fine to share the public, and encrypt against the public. -#[command(rename = "get-secret", rpc_only, metadata(authenticated = false))] -pub async fn get_secret( - #[context] ctx: SetupContext, - #[arg] pubkey: Jwk, -) -> Result { - let secret = ctx.update_secret().await?; - let mut header = josekit::jwe::JweHeader::new(); - header.set_algorithm("ECDH-ES"); - header.set_content_encryption("A256GCM"); - - let encrypter = josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs - .encrypter_from_jwk(&pubkey) - .unwrap(); - - Ok(josekit::jwe::serialize_compact(secret.as_bytes(), &header, &encrypter).unwrap()) - // Need to encrypt from the public key sent - // then encode via hex +#[command(rename = "get-pubkey", rpc_only, metadata(authenticated = false))] +pub async fn get_pubkey(#[context] ctx: SetupContext) -> Result { + let secret = ctx.current_secret.clone(); + let pub_key = secret.to_public_key()?; + Ok(pub_key) } #[command(subcommands(verify_cifs))] @@ -228,11 +229,13 @@ pub fn cifs() -> Result<(), Error> { #[command(rename = "verify", rpc_only)] pub async fn verify_cifs( + #[context] ctx: SetupContext, #[arg] hostname: String, #[arg] path: PathBuf, #[arg] username: String, - #[arg] password: Option, + #[arg] password: Option, ) -> Result { + let password: Option = password.map(|x| x.decrypt(&*ctx)).flatten(); let guard = TmpMountGuard::mount( &Cifs { hostname, @@ -252,10 +255,31 @@ pub async fn verify_cifs( pub async fn execute( #[context] ctx: SetupContext, #[arg(rename = "embassy-logicalname")] embassy_logicalname: PathBuf, - #[arg(rename = "embassy-password")] embassy_password: String, + #[arg(rename = "embassy-password")] embassy_password: EncryptedWire, #[arg(rename = "recovery-source")] mut recovery_source: Option, - #[arg(rename = "recovery-password")] recovery_password: Option, + #[arg(rename = "recovery-password")] recovery_password: Option, ) -> Result { + let embassy_password = match embassy_password.decrypt(&*ctx) { + Some(a) => a, + None => { + return Err(Error::new( + color_eyre::eyre::eyre!("Couldn't decode embassy_password"), + crate::ErrorKind::Unknown, + )) + } + }; + let recovery_password: Option = match recovery_password { + Some(a) => match a.decrypt(&*ctx) { + Some(a) => Some(a), + None => { + return Err(Error::new( + color_eyre::eyre::eyre!("Couldn't decode recovery_password"), + crate::ErrorKind::Unknown, + )) + } + }, + None => None, + }; if let Some(v2_drive) = &*ctx.selected_v2_drive.read().await { recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone()))) } diff --git a/frontend/lint-staged.config.js b/frontend/lint-staged.config.js index 4db7d0066..0470e009d 100644 --- a/frontend/lint-staged.config.js +++ b/frontend/lint-staged.config.js @@ -3,6 +3,6 @@ module.exports = { '*.ts': 'tslint --fix', 'projects/ui/**/*.ts': () => 'npm run check:ui', 'projects/shared/**/*.ts': () => 'npm run check:shared', - 'projects/diagnostic-ui/**/*.ts': () => 'npm run check:diagnostic-ui', - 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup-wizard', + 'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui', + 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup', } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e5a87d10d..66720bc93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,8 +21,6 @@ "@materia-ui/ngx-monaco-editor": "^6.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@types/aes-js": "^3.1.1", - "aes-js": "^3.1.2", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", "cbor": "npm:@jprochazk/cbor@^0.4.9", @@ -3570,11 +3568,6 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, - "node_modules/@types/aes-js": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz", - "integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==" - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -4077,11 +4070,6 @@ "node": ">=8.9.0" } }, - "node_modules/aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -17168,11 +17156,6 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, - "@types/aes-js": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz", - "integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==" - }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -17657,11 +17640,6 @@ } } }, - "aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a446d62ce..6b3c51454 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,17 +5,17 @@ "homepage": "https://start9.com/", "scripts": { "ng": "ng", - "check": "npm run check:shared && npm run check:ui && npm run check:setup-wizard && npm run check:diagnostic-ui", + "check": "npm run check:shared && npm run check:ui && npm run check:setup && npm run check:dui", "check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck", - "check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", - "check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", + "check:dui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", + "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build", - "build:diagnostic-ui": "ng run diagnostic-ui:build", - "build:setup-wizard": "ng run setup-wizard:build", + "build:dui": "ng run diagnostic-ui:build", + "build:setup": "ng run setup-wizard:build", "build:ui": "ng run ui:build", - "build:all": "npm run build:deps && npm run build:diagnostic-ui && npm run build:setup-wizard && npm run build:ui", - "start:diagnostic": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0", + "build:all": "npm run build:deps && npm run build:dui && npm run build:setup && npm run build:ui", + "start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0", "start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0", "start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0", "start:ui:proxy": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0 -- --proxy-config proxy.conf.json", @@ -35,8 +35,6 @@ "@materia-ui/ngx-monaco-editor": "^6.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@types/aes-js": "^3.1.1", - "aes-js": "^3.1.2", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", "cbor": "npm:@jprochazk/cbor@^0.4.9", diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts index a671897c5..1cac07533 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts @@ -27,7 +27,7 @@ export class CifsModal { constructor( private readonly modalController: ModalController, - private readonly apiService: ApiService, + private readonly api: ApiService, private readonly loadingCtrl: LoadingController, private readonly alertCtrl: AlertController, ) {} @@ -44,7 +44,12 @@ export class CifsModal { await loader.present() try { - const embassyOS = await this.apiService.verifyCifs(this.cifs) + const embassyOS = await this.api.verifyCifs({ + ...this.cifs, + password: this.cifs.password + ? await this.api.encrypt(this.cifs.password) + : null, + }) await loader.dismiss() diff --git a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts index 925bc3cc0..4440c1c46 100644 --- a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts @@ -8,7 +8,6 @@ import { } from '@ionic/angular' import { PasswordPage } from 'src/app/modals/password/password.page' import { ApiService } from 'src/app/services/api/api.service' -import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service' import { StateService } from 'src/app/services/state.service' import SwiperCore, { Swiper } from 'swiper' import { ErrorToastService } from '@start9labs/shared' @@ -26,8 +25,7 @@ export class HomePage { error = false constructor( - private readonly unencrypted: ApiService, - private readonly encrypted: RPCEncryptedService, + private readonly api: ApiService, private readonly modalCtrl: ModalController, private readonly alertCtrl: AlertController, private readonly loadingCtrl: LoadingController, @@ -38,8 +36,8 @@ export class HomePage { async ngOnInit() { try { - this.encrypted.secret = await this.unencrypted.getSecret() - const disks = await this.unencrypted.getDrives() + await this.api.getPubKey() + const disks = await this.api.getDrives() this.guid = disks.find(d => !!d.guid)?.guid } catch (e: any) { this.error = true diff --git a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts index 7a897566b..76ac72f5d 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts @@ -1,16 +1,30 @@ +import * as jose from 'node-jose' export abstract class ApiService { - // unencrypted + pubkey?: jose.JWK.Key + abstract getStatus(): Promise // setup.status - abstract getSecret(): Promise // setup.get-secret + abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list abstract set02XDrive(logicalname: string): Promise // setup.recovery.v2.set abstract getRecoveryStatus(): Promise // setup.recovery.status - - // encrypted abstract verifyCifs(cifs: CifsRecoverySource): Promise // setup.cifs.verify abstract importDrive(importInfo: ImportDriveReq): Promise // setup.attach abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise // setup.execute abstract setupComplete(): Promise // setup.complete + + async encrypt(toEncrypt: string): Promise { + if (!this.pubkey) throw new Error('No pubkey found!') + const encrypted = await jose.JWE.createEncrypt(this.pubkey!) + .update(toEncrypt) + .final() + return { + encrypted, + } + } +} + +type Encrypted = { + encrypted: string } export type GetStatusRes = { @@ -19,14 +33,14 @@ export type GetStatusRes = { export type ImportDriveReq = { guid: string - 'embassy-password': string + 'embassy-password': Encrypted } export type SetupEmbassyReq = { 'embassy-logicalname': string - 'embassy-password': string + 'embassy-password': Encrypted 'recovery-source': CifsRecoverySource | DiskRecoverySource | null - 'recovery-password': string | null + 'recovery-password': Encrypted | null } export type SetupEmbassyRes = { @@ -72,7 +86,7 @@ export type CifsRecoverySource = { hostname: string path: string username: string - password: string | null + password: Encrypted | null } export type DiskInfo = { diff --git a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts index a481c422e..93b49ae01 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -18,19 +18,15 @@ import { SetupEmbassyReq, SetupEmbassyRes, } from './api.service' -import { RPCEncryptedService } from '../rpc-encrypted.service' import * as jose from 'node-jose' @Injectable({ providedIn: 'root', }) -export class LiveApiService implements ApiService { - constructor( - private readonly unencrypted: HttpService, - private readonly encrypted: RPCEncryptedService, - ) {} - - // ** UNENCRYPTED ** +export class LiveApiService extends ApiService { + constructor(private readonly http: HttpService) { + super() + } async getStatus() { return this.rpcRequest({ @@ -40,24 +36,19 @@ export class LiveApiService implements ApiService { } /** - * We want to update the secret, which means that we will call in clearnet the - * getSecret, and all the information is never in the clear, and only public + * We want to update the pubkey, which means that we will call in clearnet the + * getPubKey, and all the information is never in the clear, and only public * information is sent across the network. We don't want to expose that we do * this wil all public/private key, which means that there is no information loss * through the network. */ - async getSecret() { - const keystore = jose.JWK.createKeyStore() - const key = await keystore.generate('EC', 'P-256') - const response: string = await this.rpcRequest({ - method: 'setup.get-secret', - params: { pubkey: key.toJSON() }, + async getPubKey() { + const response: jose.JWK.Key = await this.rpcRequest({ + method: 'setup.get-pubkey', + params: {}, }) - const decrypted = await jose.JWE.createDecrypt(key).decrypt(response) - const decoded = new TextDecoder().decode(decrypted.plaintext) - - return decoded + this.pubkey = response } async getDrives() { @@ -81,18 +72,16 @@ export class LiveApiService implements ApiService { }) } - // ** ENCRYPTED ** - async verifyCifs(source: CifsRecoverySource) { source.path = source.path.replace('/\\/g', '/') - return this.encrypted.rpcRequest({ + return this.rpcRequest({ method: 'setup.cifs.verify', params: source, }) } async importDrive(params: ImportDriveReq) { - const res = await this.encrypted.rpcRequest({ + const res = await this.rpcRequest({ method: 'setup.attach', params, }) @@ -110,7 +99,7 @@ export class LiveApiService implements ApiService { ].path.replace('/\\/g', '/') } - const res = await this.encrypted.rpcRequest({ + const res = await this.rpcRequest({ method: 'setup.execute', params: setupInfo, }) @@ -122,7 +111,7 @@ export class LiveApiService implements ApiService { } async setupComplete() { - const res = await this.encrypted.rpcRequest({ + const res = await this.rpcRequest({ method: 'setup.complete', params: {}, }) @@ -134,7 +123,7 @@ export class LiveApiService implements ApiService { } private async rpcRequest(opts: RPCOptions): Promise { - const res = await this.unencrypted.rpcRequest(opts) + const res = await this.http.rpcRequest(opts) const rpcRes = res.body diff --git a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts index e86607a09..93983f506 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -6,15 +6,14 @@ import { ImportDriveReq, SetupEmbassyReq, } from './api.service' +import * as jose from 'node-jose' let tries = 0 @Injectable({ providedIn: 'root', }) -export class MockApiService implements ApiService { - // ** UNENCRYPTED ** - +export class MockApiService extends ApiService { async getStatus() { await pauseFor(1000) return { @@ -22,17 +21,21 @@ export class MockApiService implements ApiService { } } - async getSecret() { + async getPubKey() { await pauseFor(1000) - const ascii = 'thisisasecret' + const keystore = jose.JWK.createKeyStore() - const arr1 = [] - for (let n = 0, l = ascii.length; n < l; n++) { - var hex = Number(ascii.charCodeAt(n)).toString(16) - arr1.push(hex) - } - return arr1.join('') + // randomly generated + // this.pubkey = await keystore.generate('EC', 'P-256') + + // generated from backend + this.pubkey = await jose.JWK.asKey({ + kty: 'EC', + crv: 'P-256', + x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4', + y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI', + }) } async getDrives() { @@ -76,8 +79,6 @@ export class MockApiService implements ApiService { } } - // ** ENCRYPTED ** - async verifyCifs(params: CifsRecoverySource) { await pauseFor(1000) return { diff --git a/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts b/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts deleted file mode 100644 index d271ff0c7..000000000 --- a/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Injectable } from '@angular/core' -import * as aesjs from 'aes-js' -import * as pbkdf2 from 'pbkdf2' -import { - HttpError, - RpcError, - HttpService, - RPCOptions, - Method, - RPCResponse, - isRpcError, -} from '@start9labs/shared' - -@Injectable({ - providedIn: 'root', -}) -export class RPCEncryptedService { - secret?: string - - constructor(private readonly http: HttpService) {} - - async rpcRequest(opts: Omit): Promise { - const encryptedBody = await AES_CTR.encryptPbkdf2( - this.secret || '', - encodeUtf8(JSON.stringify(opts)), - ) - - const res: RPCResponse = await this.http - .httpRequest({ - method: Method.POST, - url: this.http.relativeUrl, - body: encryptedBody.buffer, - responseType: 'arrayBuffer', - headers: { - 'Content-Encoding': 'aesctr256', - 'Content-Type': 'application/json', - }, - }) - .then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body)) - .then(res => JSON.parse(res)) - .catch(e => { - if (!e.status && !e.statusText) { - throw new NetworkError() - } else { - throw new HttpError(e) - } - }) - if (isRpcError(res)) throw new RpcError(res.error) - return res.result - } -} - -class NetworkError { - readonly code = null - readonly message = - 'Network Error. Please try refreshing the page or clearing your browser cache' - readonly details = null -} - -type AES_CTR = { - encryptPbkdf2: ( - secretKey: string, - messageBuffer: Uint8Array, - ) => Promise - decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise -} - -const AES_CTR: AES_CTR = { - encryptPbkdf2: async (secretKey: string, messageBuffer: Uint8Array) => { - const salt = window.crypto.getRandomValues(new Uint8Array(16)) - const counter = window.crypto.getRandomValues(new Uint8Array(16)) - - const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - - const aesCtr = new aesjs.ModeOfOperation.ctr( - key, - new aesjs.Counter(counter), - ) - const encryptedBytes = aesCtr.encrypt(messageBuffer) - return new Uint8Array([...counter, ...salt, ...encryptedBytes]) - }, - decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => { - const buff = new Uint8Array(arr) - const counter = buff.slice(0, 16) - const salt = buff.slice(16, 32) - - const cipher = buff.slice(32) - const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - - const aesCtr = new aesjs.ModeOfOperation.ctr( - key, - new aesjs.Counter(counter), - ) - const decryptedBytes = aesCtr.decrypt(cipher) - - return aesjs.utils.utf8.fromBytes(decryptedBytes) - }, -} - -function encodeUtf8(str: string): Uint8Array { - const encoder = new TextEncoder() - return encoder.encode(str) -} diff --git a/frontend/projects/setup-wizard/src/app/services/state.service.ts b/frontend/projects/setup-wizard/src/app/services/state.service.ts index b009c61f7..192a10a7f 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -30,7 +30,7 @@ export class StateService { cert = '' constructor( - private readonly apiService: ApiService, + private readonly api: ApiService, private readonly errorToastService: ErrorToastService, ) {} @@ -45,7 +45,7 @@ export class StateService { let progress try { - progress = await this.apiService.getRecoveryStatus() + progress = await this.api.getRecoveryStatus() } catch (e: any) { this.errorToastService.present({ message: `${e.message}\n\nRestart Embassy to try again.`, @@ -67,9 +67,9 @@ export class StateService { } async importDrive(guid: string, password: string): Promise { - const ret = await this.apiService.importDrive({ + const ret = await this.api.importDrive({ guid, - 'embassy-password': password, + 'embassy-password': await this.api.encrypt(password), }) this.torAddress = ret['tor-address'] this.lanAddress = ret['lan-address'] @@ -80,11 +80,13 @@ export class StateService { storageLogicalname: string, password: string, ): Promise { - const ret = await this.apiService.setupEmbassy({ + const ret = await this.api.setupEmbassy({ 'embassy-logicalname': storageLogicalname, - 'embassy-password': password, + 'embassy-password': await this.api.encrypt(password), 'recovery-source': this.recoverySource || null, - 'recovery-password': this.recoveryPassword || null, + 'recovery-password': this.recoveryPassword + ? await this.api.encrypt(this.recoveryPassword) + : null, }) this.torAddress = ret['tor-address'] this.lanAddress = ret['lan-address'] @@ -92,7 +94,7 @@ export class StateService { } async completeEmbassy(): Promise { - const ret = await this.apiService.setupComplete() + const ret = await this.api.setupComplete() this.torAddress = ret['tor-address'] this.lanAddress = ret['lan-address'] this.cert = ret['root-ca']