mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
<ng-container *ngIf="pkg$ | async as pkg">
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
|
||||
<ion-content *ngIf="pkg | toDependencies as dependencies">
|
||||
<ion-content>
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||
<app-show-progress
|
||||
*ngIf="pkg | progressData as progressData"
|
||||
[pkg]="pkg"
|
||||
[progressData]="progressData"
|
||||
></app-show-progress>
|
||||
</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
|
||||
@@ -27,14 +41,37 @@
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ion-content *ngIf="showProgress(pkg)">
|
||||
<app-show-progress
|
||||
*ngIf="pkg | progressData as progressData"
|
||||
[pkg]="pkg"
|
||||
[progressData]="progressData"
|
||||
></app-show-progress>
|
||||
</ion-content>
|
||||
<!-- 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>
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
})
|
||||
|
||||
|
||||
@@ -11,24 +11,20 @@
|
||||
</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>
|
||||
<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>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
<ion-grid>
|
||||
<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>
|
||||
<h1 class="montserrat">{{ details.name }}</h1>
|
||||
</div>
|
||||
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
|
||||
<ion-icon slot="start" name="repeat-outline"></ion-icon>
|
||||
@@ -88,4 +84,5 @@
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
margin-top: 32px;
|
||||
h1 {
|
||||
font-size: 42px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export class ConfigService {
|
||||
)
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
|
||||
isLaunchable(
|
||||
state: PackageState,
|
||||
status: PackageMainStatus,
|
||||
|
||||
Reference in New Issue
Block a user