From 9146c31abfd66a9153f2f7be2d1d3719c9556896 Mon Sep 17 00:00:00 2001 From: Lucy C <12953208+elvece@users.noreply.github.com> Date: Sat, 26 Nov 2022 09:47:00 -0700 Subject: [PATCH] get pubkey and encrypt password on login (#1965) * get pubkey and encrypt password on login * only encrypt password if insecure context * fix logic * fix secure context conditional * get-pubkey to auth api * save two lines * feat: Add the backend to the ui (#1968) * hide app show if insecure and update copy for LAN * show install progress when insecure and prevent backup and restore * ask remove USB Co-authored-by: Matt Hill Co-authored-by: J M <2364004+Blu-J@users.noreply.github.com> --- backend/src/auth.rs | 77 ++++++++-- backend/src/backup/backup_bulk.rs | 14 +- backend/src/context/cli.rs | 8 + backend/src/context/rpc.rs | 19 +++ backend/src/context/setup.rs | 23 ++- backend/src/setup.rs | 2 +- .../src/app/pages/home/home.page.ts | 3 +- .../backup-drives/backup-drives.component.ts | 2 +- .../widget-list/widget-list.component.ts | 5 +- .../apps-routes/app-show/app-show.page.html | 95 ++++++++---- .../apps-routes/app-show/app-show.page.ts | 12 +- .../ui/src/app/pages/login/login.page.ts | 17 ++- .../marketplace-list.page.html | 141 +++++++++--------- .../marketplace-list.page.scss | 1 + .../marketplace-list/marketplace-list.page.ts | 2 +- .../app/pages/server-routes/lan/lan.page.html | 27 +--- .../server-show/server-show.page.html | 17 +++ .../server-show/server-show.page.ts | 24 ++- .../app/pages/server-routes/wifi/wifi.page.ts | 2 +- .../ui/src/app/services/api/api.types.ts | 9 +- .../app/services/api/embassy-api.service.ts | 16 +- .../services/api/embassy-live-api.service.ts | 14 ++ .../services/api/embassy-mock-api.service.ts | 17 +++ .../ui/src/app/services/config.service.ts | 4 + 24 files changed, 381 insertions(+), 170 deletions(-) diff --git a/backend/src/auth.rs b/backend/src/auth.rs index e1509f6af..4e5455651 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -4,6 +4,7 @@ use std::marker::PhantomData; use chrono::{DateTime, Utc}; use clap::ArgMatches; use color_eyre::eyre::eyre; +use josekit::jwk::Jwk; use patch_db::{DbHandle, LockReceipt}; use rpc_toolkit::command; use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts}; @@ -15,11 +16,53 @@ use tracing::instrument; use crate::context::{CliContext, RpcContext}; use crate::middleware::auth::{AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken}; +use crate::middleware::encrypt::EncryptedWire; use crate::util::display_none; use crate::util::serde::{display_serializable, IoFormat}; use crate::{ensure_code, Error, ResultExt}; +#[derive(Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PasswordType { + EncryptedWire(EncryptedWire), + String(String), +} +impl PasswordType { + pub fn decrypt(self, current_secret: impl AsRef) -> Result { + match self { + PasswordType::String(x) => Ok(x), + PasswordType::EncryptedWire(x) => x.decrypt(current_secret).ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("Couldn't decode password"), + crate::ErrorKind::Unknown, + ) + }), + } + } +} +impl Default for PasswordType { + fn default() -> Self { + PasswordType::String(String::default()) + } +} +impl std::fmt::Debug for PasswordType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + Ok(()) + } +} -#[command(subcommands(login, logout, session, reset_password))] +impl std::str::FromStr for PasswordType { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match serde_json::from_str(s) { + Ok(a) => a, + Err(_) => PasswordType::String(s.to_string()), + }) + } +} + +#[command(subcommands(login, logout, session, reset_password, get_pubkey))] pub fn auth() -> Result<(), Error> { Ok(()) } @@ -50,11 +93,11 @@ fn gen_pwd() { #[instrument(skip(ctx, password))] async fn cli_login( ctx: CliContext, - password: Option, + password: Option, metadata: Value, ) -> Result<(), RpcError> { let password = if let Some(password) = password { - password + password.decrypt(&ctx)? } else { rpassword::prompt_password("Password: ")? }; @@ -107,7 +150,7 @@ pub async fn login( #[context] ctx: RpcContext, #[request] req: &RequestParts, #[response] res: &mut ResponseParts, - #[arg] password: Option, + #[arg] password: Option, #[arg( parse(parse_metadata), default = "cli_metadata", @@ -115,7 +158,7 @@ pub async fn login( )] metadata: Value, ) -> Result<(), Error> { - let password = password.unwrap_or_default(); + let password = password.unwrap_or_default().decrypt(&ctx)?; let mut handle = ctx.secret_store.acquire().await?; check_password_against_db(&mut handle, &password).await?; @@ -265,17 +308,17 @@ pub async fn kill( #[instrument(skip(ctx, old_password, new_password))] async fn cli_reset_password( ctx: CliContext, - old_password: Option, - new_password: Option, + old_password: Option, + new_password: Option, ) -> Result<(), RpcError> { let old_password = if let Some(old_password) = old_password { - old_password + old_password.decrypt(&ctx)? } else { rpassword::prompt_password("Current Password: ")? }; let new_password = if let Some(new_password) = new_password { - new_password + new_password.decrypt(&ctx)? } else { let new_password = rpassword::prompt_password("New Password: ")?; if new_password != rpassword::prompt_password("Confirm: ")? { @@ -354,11 +397,11 @@ where #[instrument(skip(ctx, old_password, new_password))] pub async fn reset_password( #[context] ctx: RpcContext, - #[arg(rename = "old-password")] old_password: Option, - #[arg(rename = "new-password")] new_password: Option, + #[arg(rename = "old-password")] old_password: Option, + #[arg(rename = "new-password")] new_password: Option, ) -> Result<(), Error> { - let old_password = old_password.unwrap_or_default(); - let new_password = new_password.unwrap_or_default(); + let old_password = old_password.unwrap_or_default().decrypt(&ctx)?; + let new_password = new_password.unwrap_or_default().decrypt(&ctx)?; let mut secrets = ctx.secret_store.acquire().await?; check_password_against_db(&mut secrets, &old_password).await?; @@ -371,3 +414,11 @@ pub async fn reset_password( Ok(()) } + +#[command(rename = "get-pubkey", display(display_none))] +#[instrument(skip(ctx))] +pub async fn get_pubkey(#[context] ctx: RpcContext) -> Result { + let secret = ctx.as_ref().clone(); + let pub_key = secret.to_public_key()?; + Ok(pub_key) +} diff --git a/backend/src/backup/backup_bulk.rs b/backend/src/backup/backup_bulk.rs index e3268a271..f2c44e640 100644 --- a/backend/src/backup/backup_bulk.rs +++ b/backend/src/backup/backup_bulk.rs @@ -125,23 +125,31 @@ fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result, + #[arg(rename = "old-password", long = "old-password")] old_password: Option< + crate::auth::PasswordType, + >, #[arg( rename = "package-ids", long = "package-ids", parse(parse_comma_separated) )] package_ids: Option>, - #[arg] password: String, + #[arg] password: crate::auth::PasswordType, ) -> Result<(), Error> { let mut db = ctx.db.handle(); + let old_password_decrypted = old_password + .as_ref() + .unwrap_or(&password) + .clone() + .decrypt(&ctx)?; + let password = password.decrypt(&ctx)?; check_password_against_db(&mut ctx.secret_store.acquire().await?, &password).await?; let fs = target_id .load(&mut ctx.secret_store.acquire().await?) .await?; let mut backup_guard = BackupMountGuard::mount( TmpMountGuard::mount(&fs, ReadWrite).await?, - old_password.as_ref().unwrap_or(&password), + &old_password_decrypted, ) .await?; let all_packages = crate::db::DatabaseModel::new() diff --git a/backend/src/context/cli.rs b/backend/src/context/cli.rs index 7ca6a6b5b..cd272d2bc 100644 --- a/backend/src/context/cli.rs +++ b/backend/src/context/cli.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use clap::ArgMatches; use color_eyre::eyre::eyre; use cookie_store::CookieStore; +use josekit::jwk::Jwk; use reqwest::Proxy; use reqwest_cookie_store::CookieStoreMutex; use rpc_toolkit::reqwest::{Client, Url}; @@ -18,6 +19,8 @@ use tracing::instrument; use crate::util::config::{load_config_from_paths, local_config_path}; use crate::ResultExt; +use super::setup::CURRENT_SECRET; + #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct CliContextConfig { @@ -126,6 +129,11 @@ impl CliContext { }))) } } +impl AsRef for CliContext { + fn as_ref(&self) -> &Jwk { + &*CURRENT_SECRET + } +} impl std::ops::Deref for CliContext { type Target = CliContextSeed; fn deref(&self) -> &Self::Target { diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index cccc40659..9bcc9235b 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -8,6 +8,7 @@ use std::time::Duration; use bollard::Docker; use helpers::to_tmp_path; +use josekit::jwk::Jwk; use patch_db::json_ptr::JsonPointer; use patch_db::{DbHandle, LockReceipt, LockType, PatchDb, Revision}; use reqwest::Url; @@ -36,6 +37,8 @@ use crate::status::{MainStatus, Status}; use crate::util::config::load_config_from_paths; use crate::{Error, ErrorKind, ResultExt}; +use super::setup::CURRENT_SECRET; + #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct RpcContextConfig { @@ -134,6 +137,7 @@ pub struct RpcContextSeed { pub open_authed_websockets: Mutex>>>, pub rpc_stream_continuations: Mutex>, pub wifi_manager: Option>>, + pub current_secret: Arc, } pub struct RpcCleanReceipts { @@ -269,6 +273,16 @@ impl RpcContext { wifi_manager: base .wifi_interface .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), + 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, + ) + })?, + ), }); let res = Self(seed); @@ -424,6 +438,11 @@ impl RpcContext { } } } +impl AsRef for RpcContext { + fn as_ref(&self) -> &Jwk { + &*CURRENT_SECRET + } +} impl Context for RpcContext {} impl Deref for RpcContext { type Target = RpcContextSeed; diff --git a/backend/src/context/setup.rs b/backend/src/context/setup.rs index f72eafcd6..8f5b39477 100644 --- a/backend/src/context/setup.rs +++ b/backend/src/context/setup.rs @@ -22,6 +22,14 @@ use crate::setup::{password_hash, SetupStatus}; use crate::util::config::load_config_from_paths; use crate::{Error, ResultExt}; +lazy_static::lazy_static! { + pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| { + tracing::debug!("{:?}", e); + tracing::error!("Couldn't generate ec key"); + panic!("Couldn't generate ec key") + }); +} + #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct SetupResult { @@ -69,9 +77,6 @@ pub struct SetupContextSeed { pub migration_prefetch_rows: usize, pub shutdown: Sender<()>, pub datadir: PathBuf, - /// Used to encrypt for hidding from snoopers for setups create password - /// Set via path - pub current_secret: Arc, pub selected_v2_drive: RwLock>, pub cached_product_key: RwLock>>, pub setup_status: RwLock>>, @@ -80,7 +85,7 @@ pub struct SetupContextSeed { impl AsRef for SetupContextSeed { fn as_ref(&self) -> &Jwk { - &self.current_secret + &*CURRENT_SECRET } } @@ -99,16 +104,6 @@ impl SetupContext { migration_prefetch_rows: cfg.migration_prefetch_rows.unwrap_or(100_000), shutdown, datadir, - 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), setup_status: RwLock::new(None), diff --git a/backend/src/setup.rs b/backend/src/setup.rs index 6d9829d50..b8d4ff13e 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -197,7 +197,7 @@ pub async fn status(#[context] ctx: SetupContext) -> Result, /// since it is fine to share the public, and encrypt against the public. #[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 secret = ctx.as_ref().clone(); let pub_key = secret.to_public_key()?; Ok(pub_key) } diff --git a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts index d9c04cd21..e367d19bc 100644 --- a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts @@ -101,7 +101,8 @@ export class HomePage { private async presentAlertReboot() { const alert = await this.alertCtrl.create({ header: 'Install Success', - message: 'Reboot your device to begin using your new Embassy', + message: + 'Remove the USB stick and reboot your device to begin using your new Embassy', buttons: [ { text: 'Reboot', diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index 6747397a6..f27314915 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -70,7 +70,7 @@ export class BackupDrivesComponent { ): void { if (target.entry.type === 'cifs' && !target.entry.mountable) { const message = - 'Unable to connect to Network Folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.' + 'Unable to connect to Network Folder. Ensure (1) target computer is connected to the same LAN as your Embassy, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.' this.presentAlertError(message) return } diff --git a/frontend/projects/ui/src/app/components/widget-list/widget-list.component.ts b/frontend/projects/ui/src/app/components/widget-list/widget-list.component.ts index ad66a4ffc..d9275c768 100644 --- a/frontend/projects/ui/src/app/components/widget-list/widget-list.component.ts +++ b/frontend/projects/ui/src/app/components/widget-list/widget-list.component.ts @@ -46,11 +46,10 @@ export class WidgetListComponent { qp: { back: 'true' }, }, { - title: 'LAN Setup', + title: 'Secure LAN', icon: 'home-outline', color: 'var(--alt-orange)', - description: - 'Install your Embassy certificate for a secure local connection', + description: `Download and trust your Embassy's certificate`, link: '/system/lan', }, { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 0ee6288b3..60e8b1b4b 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -1,40 +1,77 @@ - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + You are using an unencrypted http connection + +

+

+ Click the button below to switch to https. Your browser may warn + you that the page is insecure. You can safely bypass this + warning. It will go away after you + download and trust your Embassy's certificate. +

+ + Open https + + +
+
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index a56a7565b..dabb413ff 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { @@ -13,6 +13,8 @@ import { import { tap } from 'rxjs/operators' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' +import { DOCUMENT } from '@angular/common' +import { ConfigService } from 'src/app/services/config.service' const STATES = [ PackageState.Installing, @@ -26,6 +28,8 @@ const STATES = [ changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppShowPage { + readonly secure = this.config.isSecure() + private readonly pkgId = getPkgId(this.route) readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe( @@ -39,6 +43,8 @@ export class AppShowPage { private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly patch: PatchDB, + private readonly config: ConfigService, + @Inject(DOCUMENT) private readonly document: Document, ) {} isInstalled({ state }: PackageDataEntry): boolean { @@ -56,4 +62,8 @@ export class AppShowPage { showProgress({ state }: PackageDataEntry): boolean { return STATES.includes(state) } + + launchHttps() { + window.open(this.document.location.href.replace('http', 'https')) + } } diff --git a/frontend/projects/ui/src/app/pages/login/login.page.ts b/frontend/projects/ui/src/app/pages/login/login.page.ts index 6886a0788..3532d9aa9 100644 --- a/frontend/projects/ui/src/app/pages/login/login.page.ts +++ b/frontend/projects/ui/src/app/pages/login/login.page.ts @@ -3,6 +3,7 @@ import { LoadingController, getPlatforms } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' import { Router } from '@angular/router' +import { ConfigService } from 'src/app/services/config.service' @Component({ selector: 'login', @@ -14,14 +15,26 @@ export class LoginPage { unmasked = false error = '' loader?: HTMLIonLoadingElement + secure = this.config.isSecure() constructor( private readonly router: Router, private readonly authService: AuthService, private readonly loadingCtrl: LoadingController, private readonly api: ApiService, + private readonly config: ConfigService, ) {} + async ionViewDidEnter() { + if (!this.secure) { + try { + await this.api.getPubKey() + } catch (e: any) { + this.error = e + } + } + } + ngOnDestroy() { this.loader?.dismiss() } @@ -45,7 +58,9 @@ export class LoginPage { return } await this.api.login({ - password: this.password, + password: this.secure + ? this.password + : await this.api.encrypt(this.password), metadata: { platforms: getPlatforms() }, }) diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index a240f9e80..12d77e7a8 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -11,81 +11,78 @@ - - - - - - - - - - - -
- -

{{ details.name }}

-
- - - Change - - -
-
- - - - - + + + + +

{{ details.description }}

+
+
-
+ + + +
+ +

{{ details.name }}

+
+ + + Change + + +
+
+ + + + + - -
+ + -

All services are up to date!

- - - - - - - - - -
-
-
+

All services are up to date!

+ - - - -
-
-
+ + + + + + + +
+ + + + + + + + + +
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss index 05c6d697d..b45f5a6c4 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss @@ -2,6 +2,7 @@ margin-top: 32px; h1 { font-size: 42px; + margin-top: 0; } } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index e2fa1ffe4..c910c504a 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -58,7 +58,7 @@ export class MarketplaceListPage { // alt marketplace color = 'warning' description = - 'Warning. This is a Custom Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they may cause harm to your system. Install at your own risk.' + 'This is a Custom Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they may cause harm to your system. Install at your own risk.' } return { diff --git a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html index fbae132b4..7e0887cfa 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html @@ -1,6 +1,6 @@ - LAN Settings + Secure LAN @@ -13,25 +13,14 @@

- Connecting to your Embassy over LAN provides a lightning fast - experience and is a reliable fallback in case Tor is having problems. - To connect to your Embassy's .local address, you must: -
    -
  1. - Be connected to the same Local Area Network (LAN) as your Embassy. -
  2. -
  3. - Download and trust your Embassy's SSL Certificate Authority - (below). -
  4. -
- View the full + For a secure local connection, instructions. + >follow instructions + to download and trust your Embassy's Root Certificate Authority

@@ -39,11 +28,7 @@ -

Download Root CA

-

- Download and trust your Embassy's Root Certificate Authority to - establish a secure, https connection over LAN. -

+

Download Certificate

diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 9fb6630cc..468f1f5ff 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -15,6 +15,23 @@ + + + +

You are using unencrypted http

+

+ Click the button on the right to switch to https. Your browser may + warn you that the page is insecure. You can safely bypass this + warning. It will go away after you download and trust your Embassy's + certificate +

+
+ + Open Https + + +
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index ddafdcde8..acbf22e49 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core' +import { Component, Inject } from '@angular/core' import { AlertController, LoadingController, @@ -10,7 +10,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActivatedRoute } from '@angular/router' import { PatchDB } from 'patch-db-client' import { ServerNameService } from 'src/app/services/server-name.service' -import { firstValueFrom, Observable, of } from 'rxjs' +import { combineLatest, firstValueFrom, map, Observable, of } from 'rxjs' import { ErrorToastService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' import { ClientStorageService } from 'src/app/services/client-storage.service' @@ -22,6 +22,8 @@ import { GenericInputComponent, GenericInputOptions, } from 'src/app/modals/generic-input/generic-input.component' +import { ConfigService } from 'src/app/services/config.service' +import { DOCUMENT } from '@angular/common' @Component({ selector: 'server-show', @@ -36,6 +38,8 @@ export class ServerShowPage { readonly showUpdate$ = this.eosService.showUpdate$ readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$ + readonly secure = this.config.isSecure() + constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, @@ -50,6 +54,8 @@ export class ServerShowPage { private readonly serverNameService: ServerNameService, private readonly authService: AuthService, private readonly toastCtrl: ToastController, + private readonly config: ConfigService, + @Inject(DOCUMENT) private readonly document: Document, ) {} async presentModalName(): Promise { @@ -202,6 +208,10 @@ export class ServerShowPage { await alert.present() } + launchHttps() { + window.open(this.document.location.href.replace('http', 'https')) + } + addClick(title: string) { switch (title) { case 'Manage': @@ -353,7 +363,7 @@ export class ServerShowPage { action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }), detail: true, - disabled$: of(false), + disabled$: of(!this.secure), }, { title: 'Restore From Backup', @@ -362,7 +372,10 @@ export class ServerShowPage { action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }), detail: true, - disabled$: this.eosService.updatingOrBackingUp$, + disabled$: combineLatest([ + this.eosService.updatingOrBackingUp$, + of(this.secure), + ]).pipe(map(([updating, secure]) => updating || !secure)), }, ], Manage: [ @@ -387,8 +400,7 @@ export class ServerShowPage { }, { title: 'LAN', - description: - 'Install your Embassy certificate for a secure local connection', + description: `Download and trust your Embassy's certificate for a secure local connection`, icon: 'home-outline', action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 3ea8f5931..b42b8b8e2 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -61,7 +61,7 @@ export class WifiPage { const alert = await this.alertCtrl.create({ header: 'Cannot Complete Action', message: - 'You must be connected to your Emassy via LAN to change the country.', + 'You must be connected to your Embassy via LAN to change the country.', buttons: [ { text: 'OK', diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index a60e5d782..207d0415c 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -21,7 +21,10 @@ export module RR { // auth - export type LoginReq = { password: string; metadata: SessionMetadata } // auth.login - unauthed + export type LoginReq = { + password: Encrypted | string + metadata: SessionMetadata + } // auth.login - unauthed export type loginRes = null export type LogoutReq = {} // auth.logout @@ -451,3 +454,7 @@ declare global { parse(text: Stringified, reviver?: (key: any, value: any) => any): T } } + +export type Encrypted = { + encrypted: string +} diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index a5823d076..ded8b075b 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,12 +1,24 @@ import { BehaviorSubject, Observable } from 'rxjs' import { Update } from 'patch-db-client' -import { RR } from './api.types' +import { RR, Encrypted } from './api.types' import { DataModel } from 'src/app/services/patch-db/data-model' import { Log } from '@start9labs/shared' import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import * as jose from 'node-jose' export abstract class ApiService { readonly patchStream$ = new BehaviorSubject[]>([]) + pubkey?: jose.JWK.Key + + 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, + } + } // http @@ -25,6 +37,8 @@ export abstract class ApiService { // auth + abstract getPubKey(): Promise + abstract login(params: RR.LoginReq): Promise abstract logout(params: RR.LogoutReq): Promise diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 87d519137..f61bc1bdb 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -33,6 +33,7 @@ export class LiveApiService extends ApiService { ; (window as any).rpcClient = this } + // for getting static files: ex icons, instructions, licenses async getStatic(url: string): Promise { return this.httpRequest({ method: Method.GET, @@ -41,6 +42,7 @@ export class LiveApiService extends ApiService { }) } + // for sideloading packages async uploadPackage(guid: string, body: ArrayBuffer): Promise { return this.httpRequest({ method: Method.POST, @@ -63,6 +65,18 @@ export class LiveApiService extends ApiService { // auth + /** + * 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. + */ + async getPubKey() { + this.pubkey = await this.rpcRequest({ + method: 'auth.get-pubkey', + params: {}, + }) + } + async login(params: RR.LoginReq): Promise { return this.rpcRequest({ method: 'auth.login', params }, false) } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index abccef4b5..43fd33245 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -38,6 +38,7 @@ import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { AuthService } from '../auth.service' import { ConnectionService } from '../connection.service' import { StoreInfo } from '@start9labs/marketplace' +import * as jose from 'node-jose' const PROGRESS: InstallProgress = { size: 120, @@ -113,6 +114,22 @@ export class MockApiService extends ApiService { // auth + async getPubKey() { + await pauseFor(1000) + + // randomly generated + // const keystore = jose.JWK.createKeyStore() + // 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 login(params: RR.LoginReq): Promise { await pauseFor(2000) diff --git a/frontend/projects/ui/src/app/services/config.service.ts b/frontend/projects/ui/src/app/services/config.service.ts index 30e98f1d2..997b78c46 100644 --- a/frontend/projects/ui/src/app/services/config.service.ts +++ b/frontend/projects/ui/src/app/services/config.service.ts @@ -47,6 +47,10 @@ export class ConfigService { ) } + isSecure(): boolean { + return window.isSecureContext || this.isTor() + } + isLaunchable( state: PackageState, status: PackageMainStatus,