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

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