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 <matthewonthemoon@gmail.com>
Co-authored-by: J M <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
Lucy C
2022-11-26 09:47:00 -07:00
committed by Aiden McClelland
parent bd4c431eb4
commit 9146c31abf
24 changed files with 381 additions and 170 deletions

View File

@@ -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<Jwk>) -> Result<String, Error> {
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, "<REDACTED_PASSWORD>")?;
Ok(())
}
}
#[command(subcommands(login, logout, session, reset_password))]
impl std::str::FromStr for PasswordType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<String>,
password: Option<PasswordType>,
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<String>,
#[arg] password: Option<PasswordType>,
#[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<String>,
new_password: Option<String>,
old_password: Option<PasswordType>,
new_password: Option<PasswordType>,
) -> 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<String>,
#[arg(rename = "new-password")] new_password: Option<String>,
#[arg(rename = "old-password")] old_password: Option<PasswordType>,
#[arg(rename = "new-password")] new_password: Option<PasswordType>,
) -> 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<Jwk, RpcError> {
let secret = ctx.as_ref().clone();
let pub_key = secret.to_public_key()?;
Ok(pub_key)
}

View File

@@ -125,23 +125,31 @@ fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result<BTreeSet<PackageId
pub async fn backup_all(
#[context] ctx: RpcContext,
#[arg(rename = "target-id")] target_id: BackupTargetId,
#[arg(rename = "old-password", long = "old-password")] old_password: Option<String>,
#[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<BTreeSet<PackageId>>,
#[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()

View File

@@ -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<Jwk> for CliContext {
fn as_ref(&self) -> &Jwk {
&*CURRENT_SECRET
}
}
impl std::ops::Deref for CliContext {
type Target = CliContextSeed;
fn deref(&self) -> &Self::Target {

View File

@@ -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<BTreeMap<HashSessionToken, Vec<oneshot::Sender<()>>>>,
pub rpc_stream_continuations: Mutex<BTreeMap<RequestGuid, RpcContinuation>>,
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>,
}
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<Jwk> for RpcContext {
fn as_ref(&self) -> &Jwk {
&*CURRENT_SECRET
}
}
impl Context for RpcContext {}
impl Deref for RpcContext {
type Target = RpcContextSeed;

View File

@@ -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<Jwk>,
pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>,
pub setup_status: RwLock<Option<Result<SetupStatus, RpcError>>>,
@@ -80,7 +85,7 @@ pub struct SetupContextSeed {
impl AsRef<Jwk> 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),

View File

@@ -197,7 +197,7 @@ pub async fn status(#[context] ctx: SetupContext) -> Result<Option<SetupStatus>,
/// 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<Jwk, RpcError> {
let secret = ctx.current_secret.clone();
let secret = ctx.as_ref().clone();
let pub_key = secret.to_public_key()?;
Ok(pub_key)
}

View File

@@ -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',

View File

@@ -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
}

View File

@@ -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',
},
{

View File

@@ -1,40 +1,77 @@
<ng-container *ngIf="pkg$ | async as pkg">
<app-show-header [pkg]="pkg"></app-show-header>
<ion-content *ngIf="pkg | toDependencies as dependencies">
<ion-item-group *ngIf="pkg | toStatus as status">
<!-- ** status ** -->
<app-show-status
[pkg]="pkg"
[dependencies]="dependencies"
[status]="status"
></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="dependencies.length"
[dependencies]="dependencies"
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
<ion-content>
<!-- ** installing, updating, restoring ** -->
<ion-content *ngIf="showProgress(pkg)">
<ng-container *ngIf="showProgress(pkg); else installed">
<app-show-progress
*ngIf="pkg | progressData as progressData"
[pkg]="pkg"
[progressData]="progressData"
></app-show-progress>
</ion-content>
</ng-container>
<!-- Installed -->
<ng-template #installed>
<!-- SECURE -->
<ng-container *ngIf="secure; else insecure">
<ng-container *ngIf="pkg | toDependencies as dependencies">
<ion-item-group *ngIf="pkg | toStatus as status">
<!-- ** status ** -->
<app-show-status
[pkg]="pkg"
[dependencies]="dependencies"
[status]="status"
></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="dependencies.length"
[dependencies]="dependencies"
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-container>
</ng-container>
<!-- INSECURE -->
<ng-template #insecure>
<ion-grid style="height: 100%; max-width: 540px">
<ion-row class="ion-align-items-center" style="height: 90%">
<ion-col class="ion-text-center">
<h2>
<ion-text color="warning">
You are using an unencrypted http connection
</ion-text>
</h2>
<p class="ion-padding-bottom">
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
<a
[routerLink]="['/system', 'lan']"
style="color: var(--ion-color-dark)"
>download and trust your Embassy's certificate</a
>.
</p>
<ion-button (click)="launchHttps()">
Open https
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</ng-template>
</ion-content>
</ng-container>

View File

@@ -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<DataModel>,
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'))
}
}

View File

@@ -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() },
})

View File

@@ -11,81 +11,78 @@
</ion-header>
<ion-content class="ion-padding">
<ion-grid *ngIf="details$ | async as details">
<ion-row>
<ion-col size-lg="10" offset-lg="1" size-sm="12">
<ion-item class="description" [color]="details.color">
<ion-icon
text-wrap
size="large"
name="information-circle-outline"
></ion-icon>
<ion-label [innerHtml]="details.description"></ion-label>
</ion-item>
</ion-col>
</ion-row>
<ion-row>
<ion-col size="12">
<div class="heading">
<store-icon class="icon" [url]="details.url"></store-icon>
<h1 class="montserrat ion-text-center">{{ details.name }}</h1>
</div>
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
<ion-icon slot="start" name="repeat-outline"></ion-icon>
Change
</ion-button>
<marketplace-search [(query)]="query"></marketplace-search>
</ion-col>
</ion-row>
<ion-row class="ion-align-items-center">
<ion-col size="12">
<ng-container *ngIf="store$ | async as store; else loading">
<ng-container *ngIf="localPkgs$ | async as localPkgs">
<marketplace-categories
[categories]="store.categories"
[category]="category"
[updatesAvailable]="
(store.packages | filterPackages: '':'updates':localPkgs).length
"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<ng-container *ngIf="details$ | async as details">
<ion-item [color]="details.color">
<ion-icon slot="start" name="information-circle-outline"></ion-icon>
<ion-label>
<h2 style="font-weight: 600">{{ details.description }}</h2>
</ion-label>
</ion-item>
<div class="divider"></div>
<ion-grid>
<ion-row>
<ion-col size="12">
<div class="heading">
<store-icon class="icon" [url]="details.url"></store-icon>
<h1 class="montserrat">{{ details.name }}</h1>
</div>
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
<ion-icon slot="start" name="repeat-outline"></ion-icon>
Change
</ion-button>
<marketplace-search [(query)]="query"></marketplace-search>
</ion-col>
</ion-row>
<ion-row class="ion-align-items-center">
<ion-col size="12">
<ng-container *ngIf="store$ | async as store; else loading">
<ng-container *ngIf="localPkgs$ | async as localPkgs">
<marketplace-categories
[categories]="store.categories"
[category]="category"
[updatesAvailable]="
(store.packages | filterPackages: '':'updates':localPkgs).length
"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<ion-grid
*ngIf="store.packages | filterPackages: query:category:localPkgs as filtered"
>
<div
*ngIf="!filtered.length && category === 'updates'"
class="ion-padding"
<div class="divider"></div>
<ion-grid
*ngIf="store.packages | filterPackages: query:category:localPkgs as filtered"
>
<h1>All services are up to date!</h1>
</div>
<ion-row>
<ion-col
*ngFor="let pkg of filtered"
sizeXs="12"
sizeSm="12"
sizeMd="6"
<div
*ngIf="!filtered.length && category === 'updates'"
class="ion-padding"
>
<marketplace-item [pkg]="pkg">
<marketplace-status
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</marketplace-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ng-container>
<h1>All services are up to date!</h1>
</div>
<ng-template #loading>
<marketplace-skeleton></marketplace-skeleton>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
<ion-row>
<ion-col
*ngFor="let pkg of filtered"
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<marketplace-item [pkg]="pkg">
<marketplace-status
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</marketplace-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ng-container>
<ng-template #loading>
<marketplace-skeleton></marketplace-skeleton>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ion-content>

View File

@@ -2,6 +2,7 @@
margin-top: 32px;
h1 {
font-size: 42px;
margin-top: 0;
}
}

View File

@@ -58,7 +58,7 @@ export class MarketplaceListPage {
// alt marketplace
color = 'warning'
description =
'Warning. This is a <b>Custom</b> Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they may cause harm to your system. <b>Install at your own risk</b>.'
'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 {

View File

@@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>LAN Settings</ion-title>
<ion-title>Secure LAN</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
@@ -13,25 +13,14 @@
<ion-item class="ion-padding-bottom">
<ion-label>
<h2>
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:
<ol>
<li>
Be connected to the same Local Area Network (LAN) as your Embassy.
</li>
<li>
Download and trust your Embassy's SSL Certificate Authority
(below).
</li>
</ol>
View the full
For a secure local connection,
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
>instructions</a
>.
>follow instructions</a
>
to download and trust your Embassy's Root Certificate Authority
</h2>
</ion-label>
</ion-item>
@@ -39,11 +28,7 @@
<ion-item button (click)="installCert()">
<ion-icon slot="start" name="download-outline" size="large"></ion-icon>
<ion-label>
<h1>Download Root CA</h1>
<p>
Download and trust your Embassy's Root Certificate Authority to
establish a secure, https connection over LAN.
</p>
<h1>Download Certificate</h1>
</ion-label>
</ion-item>
</ion-item-group>

View File

@@ -15,6 +15,23 @@
<!-- loaded -->
<ion-item-group *ngIf="server$ | async as server; else loading">
<ion-item *ngIf="!secure" color="warning">
<ion-icon slot="start" name="warning-outline"></ion-icon>
<ion-label>
<h2 style="font-weight: bold">You are using unencrypted http</h2>
<p style="font-weight: 600">
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
</p>
</ion-label>
<ion-button slot="end" color="light" (click)="launchHttps()">
Open Https
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</ion-item>
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider>
<ion-text color="dark" (click)="addClick(cat.key)">

View File

@@ -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<void> {
@@ -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 }),

View File

@@ -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',

View File

@@ -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<T>(text: Stringified<T>, reviver?: (key: any, value: any) => any): T
}
}
export type Encrypted = {
encrypted: string
}

View File

@@ -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<Update<DataModel>[]>([])
pubkey?: jose.JWK.Key
async encrypt(toEncrypt: string): Promise<Encrypted> {
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<void>
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>

View File

@@ -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<string> {
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<string> {
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<RR.loginRes> {
return this.rpcRequest({ method: 'auth.login', params }, false)
}

View File

@@ -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<RR.loginRes> {
await pauseFor(2000)

View File

@@ -47,6 +47,10 @@ export class ConfigService {
)
}
isSecure(): boolean {
return window.isSecureContext || this.isTor()
}
isLaunchable(
state: PackageState,
status: PackageMainStatus,