remove product key from setup flow (#1750)

* remove product key flow from setup

* feat: backend turned off encryption + new Id + no package id

* implement new encryption scheme in FE

* decode response string

* crypto not working

* update setup wizard closes #1762

* feat: Get the encryption key

* fix: Get to recovery

* remove old code

* fix build

* fix: Install works for now

* fix bug in config for adding new list items

* dismiss action modal on success

* clear button in config

* wip: Currently broken in avahi mdns

* include headers with req/res and refactor patchDB init and usage

* fix: Can now run in the main

* flatline on failed init

* update patch DB

* add last-wifi-region to data model even though not used by FE

* chore: Fix the start.

* wip: Fix wrong order for getting hostname before sql has been
created

* fix edge case where union keys displayed as new when not new

* fix: Can start

* last backup color, markdown links always new tab, fix bug with login

* refactor to remove WithRevision

* resolve circular dep issue

* update submodule

* fix patch-db

* update patchDB

* update patch again

* escape error

* decodeuricomponent

* increase proxy buffer size

* increase proxy buffer size

* fix nginx

Co-authored-by: BluJ <mogulslayer@gmail.com>
Co-authored-by: BluJ <dragondef@gmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2022-09-07 09:25:01 -06:00
committed by GitHub
parent 76682ebef0
commit 50111e37da
175 changed files with 11436 additions and 2906 deletions

View File

@@ -1,45 +1,32 @@
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
const routes: Routes = [
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
{
path: 'product-key',
loadChildren: () =>
import('./pages/product-key/product-key.module').then(
m => m.ProductKeyPageModule,
),
},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
canActivate: [NavGuard],
},
{
path: 'recover',
loadChildren: () =>
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
canActivate: [RecoveryNavGuard],
},
{
path: 'embassy',
loadChildren: () =>
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
canActivate: [NavGuard],
},
{
path: 'loading',
loadChildren: () =>
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
canActivate: [NavGuard],
},
{
path: 'success',
loadChildren: () =>
import('./pages/success/success.module').then(m => m.SuccessPageModule),
canActivate: [NavGuard],
},
]

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from './services/state.service'
@Component({
selector: 'app-root',
@@ -14,21 +13,12 @@ export class AppComponent {
private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService,
private readonly navCtrl: NavController,
private readonly stateService: StateService,
) {}
async ngOnInit() {
try {
const status = await this.apiService.getStatus()
if (status.migrating || status['product-key']) {
this.stateService.hasProductKey = true
this.stateService.isMigrating = status.migrating
await this.navCtrl.navigateForward(`/product-key`)
} else {
this.stateService.hasProductKey = false
this.stateService.isMigrating = false
await this.navCtrl.navigateForward(`/recover`)
}
const { migrating } = await this.apiService.getStatus()
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
} catch (e: any) {
this.errorToastService.present(e)
}

View File

@@ -1,4 +1,4 @@
import { ErrorHandler, NgModule } from '@angular/core'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router'
import { HttpClientModule } from '@angular/common/http'
@@ -15,8 +15,6 @@ import { AppRoutingModule } from './app-routing.module'
import { SuccessPageModule } from './pages/success/success.module'
import { HomePageModule } from './pages/home/home.module'
import { LoadingPageModule } from './pages/loading/loading.module'
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
import { RecoverPageModule } from './pages/recover/recover.module'
import { WorkspaceConfig } from '@start9labs/shared'
@@ -35,8 +33,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
SuccessPageModule,
HomePageModule,
LoadingPageModule,
ProdKeyModalModule,
ProductKeyPageModule,
RecoverPageModule,
],
providers: [

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router'
import { RPCEncryptedService } from '../services/rpc-encrypted.service'
import { StateService } from '../services/state.service'
@Injectable({
providedIn: 'root',
})
export class NavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}
@Injectable({
providedIn: 'root',
})
export class RecoveryNavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
private readonly stateService: StateService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey || !this.stateService.hasProductKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}

View File

@@ -1,7 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import {
DiskInfo,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
@@ -15,7 +14,7 @@ import * as argon2 from '@start9labs/argon2'
export class PasswordPage {
@ViewChild('focusInput') elem?: IonInput
@Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() storageDrive?: DiskInfo
@Input() storageDrive = false
pwError = ''
password = ''

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProdKeyModal } from './prod-key-modal.page'
@NgModule({
declarations: [
ProdKeyModal,
],
imports: [
CommonModule,
FormsModule,
IonicModule,
],
exports: [
ProdKeyModal,
],
})
export class ProdKeyModalModule { }

View File

@@ -1,41 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>
Enter Product Key
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form (ngSubmit)="verifyProductKey()">
<div style="padding: 8px 24px;">
<div style="padding-bottom: 16px;">
<p>Enter your 0.2.x Product Key to establish an encrypted connection with your new Embassy.</p>
</div>
<ion-item>
<ion-input
#focusInput
[(ngModel)]="productKey"
placeholder="Enter Product Key"
maxlength="12"
></ion-input>
</ion-item>
<div style="height: 16px;">
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ error }}</p>
</div>
</div>
<input type="submit" style="display: none" />
</form>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
Cancel
</ion-button>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
Submit
</ion-button>
</ion-toolbar>
</ion-footer>

View File

@@ -1,3 +0,0 @@
ion-content {
--ion-text-color: var(--ion-color-dark);
}

View File

@@ -1,54 +0,0 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
@Component({
selector: 'prod-key-modal',
templateUrl: 'prod-key-modal.page.html',
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@ViewChild('focusInput') elem?: IonInput
@Input() target!: DiskBackupTarget
error = ''
productKey = ''
unmasked = false
constructor(
private readonly modalController: ModalController,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ngAfterViewInit() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async verifyProductKey() {
if (!this.productKey || !this.target.logicalname) return
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
await this.apiService.set02XDrive(this.target.logicalname)
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
this.modalController.dismiss({ productKey: this.productKey }, 'success')
} catch (e) {
this.encrypted.productKey = undefined
this.error = 'Invalid Product Key'
} finally {
loader.dismiss()
}
}
cancel() {
this.modalController.dismiss()
}
}

View File

@@ -64,12 +64,7 @@ export class EmbassyPage {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
buttons: ['OK'],
})
await alert.present()
}
@@ -95,40 +90,45 @@ export class EmbassyPage {
text: 'Continue',
handler: () => {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
} else {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
}
}
private async presentModalPassword(drive: DiskInfo): Promise<void> {
private async presentModalPassword(logicalname: string): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
storageDrive: drive,
storageDrive: true,
},
})
modal.onDidDismiss().then(async ret => {
if (!ret.data || !ret.data.password) return
this.setupEmbassy(drive, ret.data.password)
this.setupEmbassy(logicalname, ret.data.password)
})
await modal.present()
}
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
private async setupEmbassy(
logicalname: string,
password: string,
): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Initializing data drive. This could take a while...',
})
@@ -136,7 +136,7 @@ export class EmbassyPage {
await loader.present()
try {
await this.stateService.setupEmbassy(drive.logicalname, password)
await this.stateService.setupEmbassy(logicalname, password)
if (!!this.stateService.recoverySource) {
await this.navCtrl.navigateForward(`/loading`)
} else {

View File

@@ -4,9 +4,8 @@ import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { HomePage } from './home.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { HomePageRoutingModule } from './home-routing.module'
import { SwiperModule } from 'swiper/angular'
@NgModule({
imports: [
@@ -15,7 +14,8 @@ import { HomePageRoutingModule } from './home-routing.module'
IonicModule,
HomePageRoutingModule,
PasswordPageModule,
SwiperModule,
],
declarations: [HomePage],
})
export class HomePageModule { }
export class HomePageModule {}

View File

@@ -2,36 +2,78 @@
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
<div style="padding-bottom: 32px">
<img src="assets/img/logo.png" style="max-width: 240px" />
</div>
<ion-card color="dark">
<ion-card-content class="ion-margin">
<!-- fresh -->
<ion-card
routerLink="/embassy"
<ion-card-header>
<ion-button
*ngIf="swiper?.activeIndex === 1"
class="back-button"
fill="clear"
color="light"
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
(click)="previous()"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</ion-card-header>
<!-- recover -->
</ion-card>
<ion-card
routerLink="/recover"
color="light"
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Recover</ion-card-title>
<ion-card-subtitle>Restore from backup or recover an old Embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
</ion-button>
<ion-card-title>
Embassy Setup
<span *ngIf="swiper?.activeIndex === 1"> (recover)</span>
</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin-bottom">
<swiper (swiper)="setSwiperInstance($event)">
<ng-template swiperSlide>
<ion-item
button
[disabled]="error"
detail="true"
routerLink="/embassy"
>
<ion-icon color="dark" slot="start" name="add"></ion-icon>
<ion-label>
<h2><ion-text color="success">Start Fresh</ion-text></h2>
<p>Get started with a brand new Embassy</p>
</ion-label>
</ion-item>
<ion-item
button
[disabled]="error"
detail="true"
lines="none"
(click)="next()"
>
<ion-icon color="dark" slot="start" name="reload"></ion-icon>
<ion-label>
<h2><ion-text color="danger">Recover</ion-text></h2>
<p>
Restore from backup or use an existing Embassy data drive
</p>
</ion-label>
</ion-item>
</ng-template>
<ng-template swiperSlide>
<ion-item button detail="true" routerLink="/recover">
<ion-icon color="dark" slot="start" name="save"></ion-icon>
<ion-label>
<h2>
<ion-text color="warning">Restore From Backup</ion-text>
</h2>
<p>Recover an Embassy from encrypted backup</p>
</ion-label>
</ion-item>
<ion-item button detail="true" lines="none" (click)="import()">
<ion-icon color="dark" slot="start" name="cube"></ion-icon>
<ion-label>
<h2>
<ion-text color="primary">Use Existing Drive</ion-text>
</h2>
<p>Attach and use a valid Embassy data drive</p>
</ion-label>
</ion-item>
</ng-template>
</swiper>
</ion-card-content>
</ion-card>
</ion-col>

View File

@@ -0,0 +1,15 @@
.back-button {
position: absolute;
left: 16px;
top: 24px;
z-index: 1000000;
}
ion-item {
--background: var(--ion-color-medium);
--color: var(--ion-color-dark);
}
p {
color: var(--ion-color-dark);
}

View File

@@ -1,9 +1,112 @@
import { Component } from '@angular/core'
import {
AlertController,
IonicSlides,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { PasswordPage } from 'src/app/modals/password/password.page'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
import SwiperCore, { Swiper } from 'swiper'
import { ErrorToastService } from '@start9labs/shared'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage { }
export class HomePage {
swiper?: Swiper
guid?: string | null
error = false
constructor(
private readonly unencrypted: ApiService,
private readonly encrypted: RPCEncryptedService,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly stateService: StateService,
private readonly navCtrl: NavController,
private readonly errToastService: ErrorToastService,
) {}
async ngOnInit() {
try {
this.encrypted.secret = await this.unencrypted.getSecret()
const { disks } = await this.unencrypted.getDrives()
this.guid = disks.find(d => !!d.guid)?.guid
} catch (e: any) {
this.error = true
this.errToastService.present(e)
}
}
async ionViewDidEnter() {
if (this.swiper) {
this.swiper.allowTouchMove = false
}
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
next() {
this.swiper?.slideNext(500)
}
previous() {
this.swiper?.slidePrev(500)
}
async import() {
if (this.guid) {
const modal = await this.modalCtrl.create({
component: PasswordPage,
componentProps: { storageDrive: true },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(res.data.password)
}
})
await modal.present()
} else {
const alert = await this.alertCtrl.create({
header: 'Drive Not Found',
message:
'Please make sure the drive is a valid Embassy data drive (not a backup) and is firmly connected, then refresh the page.',
})
await alert.present()
}
}
private async importDrive(password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(this.guid!, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errToastService.present(e)
} finally {
loader.dismiss()
}
}
}
function decodeHex(hex: string) {
let str = ''
for (let n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substring(n, 2), 16))
}
return str
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ProductKeyPage } from './product-key.page'
const routes: Routes = [
{
path: '',
component: ProductKeyPage,
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProductKeyPageRoutingModule { }

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProductKeyPage } from './product-key.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProductKeyPageRoutingModule } from './product-key-routing.module'
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ProductKeyPageRoutingModule,
PasswordPageModule,
],
declarations: [ProductKeyPage],
})
export class ProductKeyPageModule { }

View File

@@ -1,43 +0,0 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
</div>
<ion-card color="dark">
<ion-card-header style="padding-bottom: 8px;">
<ion-card-title>Product Key</ion-card-title>
<ion-card-subtitle>Enter your product key to establish an encrypted connection with your Embassy</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin">
<form (submit)="submit()" style="margin-bottom: 12px;">
<ion-item-group class="ion-padding-bottom">
<ion-item color="dark">
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
<ion-input
#focusInput
name="productKey"
[(ngModel)]="productKey"
(ionChange)="error = ''"
maxlength="12"
>
</ion-input>
</ion-item>
<div class="ion-text-left">
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
</div>
</ion-item-group>
<ion-button type="submit" color="light" class="claim-button">
Submit
</ion-button>
</form>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,5 +0,0 @@
ion-item {
--border-style: solid;
--border-width: 1px;
--border-color: var(--ion-color-medium);
}

View File

@@ -1,52 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { IonInput, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
@Component({
selector: 'app-product-key',
templateUrl: 'product-key.page.html',
styleUrls: ['product-key.page.scss'],
})
export class ProductKeyPage {
@ViewChild('focusInput') elem?: IonInput
productKey = ''
error = ''
constructor(
private readonly navCtrl: NavController,
private readonly stateService: StateService,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ionViewDidEnter() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async submit() {
if (!this.productKey) return (this.error = 'Must enter product key')
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
if (this.stateService.isMigrating) {
await this.navCtrl.navigateForward(`/loading`)
} else {
await this.navCtrl.navigateForward(`/home`)
}
} catch (e) {
this.error = 'Invalid Product Key'
this.encrypted.productKey = undefined
} finally {
loader.dismiss()
}
}
}

View File

@@ -5,7 +5,6 @@ import { FormsModule } from '@angular/forms'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { DriveStatusComponent, RecoverPage } from './recover.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
import { RecoverPageRoutingModule } from './recover-routing.module'
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
@@ -17,7 +16,6 @@ import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
IonicModule,
RecoverPageRoutingModule,
PasswordPageModule,
ProdKeyModalModule,
UnitConversionPipesModule,
CifsModalModule,
],

View File

@@ -1,17 +1,10 @@
import { Component, Input } from '@angular/core'
import {
AlertController,
IonicSafeString,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@Component({
selector: 'app-recover',
@@ -21,7 +14,6 @@ import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
export class RecoverPage {
loading = true
mappedDrives: MappedDisk[] = []
hasShownGuidAlert = false
constructor(
private readonly apiService: ApiService,
@@ -29,8 +21,7 @@ export class RecoverPage {
private readonly modalCtrl: ModalController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService,
private readonly errToastService: ErrorToastService,
private readonly stateService: StateService,
) {}
@@ -44,10 +35,7 @@ export class RecoverPage {
}
driveClickable(mapped: MappedDisk) {
return (
mapped.drive['embassy-os']?.full &&
(this.stateService.hasProductKey || mapped.is02x)
)
return mapped.drive['embassy-os']?.full
}
async getDrives() {
@@ -89,50 +77,8 @@ export class RecoverPage {
})
await alert.present()
}
const importableDrive = disks.find(d => !!d.guid)
if (
!!importableDrive &&
this.stateService.hasProductKey &&
!this.hasShownGuidAlert
) {
const alert = await this.alertCtrl.create({
header: 'Embassy Data Drive Detected',
message: new IonicSafeString(
`<strong>${importableDrive.vendor || 'Unknown Vendor'} - ${
importableDrive.model || 'Unknown Model'
}</strong> contains Embassy data.
<p>To use this drive and its data, select <strong>"USE DRIVE"</strong>. This will complete the setup process.
<p><strong style="color:red">Important!</strong><br><br>
If you are trying to restore from a backup or update from 0.2.x, <strong>DO NOT</strong> select "USE DRIVE". Instead, select <strong>"CANCEL"</strong> and follow instructions.`,
),
buttons: [
{
role: 'cancel',
text: 'Cancel',
},
{
text: 'Use Drive',
handler: async () => {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { storageDrive: importableDrive },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(importableDrive.guid!, res.data.password)
}
})
await modal.present()
},
},
],
})
await alert.present()
this.hasShownGuidAlert = true
}
} catch (e: any) {
this.errorToastService.present(e)
this.errToastService.present(e)
} finally {
this.loading = false
}
@@ -165,65 +111,20 @@ export class RecoverPage {
if (!logicalname) return
if (this.stateService.hasProductKey) {
if (is02x) {
this.selectRecoverySource(logicalname)
} else {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
}
})
await modal.present()
}
// if no product key, it means they are an upgrade kit user
if (is02x) {
this.selectRecoverySource(logicalname)
} else {
if (!is02x) {
const alert = await this.alertCtrl.create({
header: 'Error',
message:
'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.productKey) {
this.selectRecoverySource(logicalname)
}
})
await modal.present()
}
}
}
private async importDrive(guid: string, password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(guid, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errorToastService.present(e)
} finally {
loader.dismiss()
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
}
})
await modal.present()
}
}

View File

@@ -9,174 +9,143 @@
name="checkmark-circle-outline"
></ion-icon>
<ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle
><b
>You have successully claimed your Embassy!</b
></ion-card-subtitle
>
<br />
</ion-card-header>
<ion-card-content>
<br />
<ng-template
[ngIf]="recoverySource && recoverySource.type === 'disk'"
<br />
<h2
*ngIf="recoverySource && recoverySource.type === 'disk'"
class="ion-padding-bottom"
>
<h2>You can now safely unplug your backup drive.</h2>
</ng-template>
<h2>
You have successully claimed your Embassy! You can now access your
device using the methods below.
You can now safely unplug your backup drive.
</h2>
<h2 style="font-weight: bold">
Access your Embassy using the methods below. You should
<a (click)="download()" class="inline">
download this page <ion-icon name="download-outline"></ion-icon>
</a>
for your records.
</h2>
<br />
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will no
longer work.
</p>
<div class="line"></div>
<!-- LAN Instructions -->
<div (click)="toggleLan()" class="toggle-label">
<h2>From Home (LAN)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<h1><b>From Home (LAN)</b></h1>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': lanOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are conncted to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are conncted to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ lanAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(lanAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
Your browser will warn you that the website is untrusted. You
can bypass this warning on most browsers. The warning will go
away after you
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
>
<b>download and trust</b>
</a>
your Embassy's Root Certificate Authority.
</p>
<ion-button
style="margin-top: 24px; margin-bottom: 24px"
color="light"
(click)="installCert()"
>
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<!-- Tor Instructions -->
<div (click)="toggleTor()" class="toggle-label">
<h2>On The Go (Tor)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will
no longer work.
</p>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': torOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ torAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
This address will only work from a
<a
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
>
<b>Tor-enabled browser</b> </a
>.
</p>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<div class="ion-text-center ion-padding-top">
<ion-button
color="light"
fill="clear"
color="primary"
strong
(click)="download()"
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
Download this page
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ lanAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
[href]="lanAddress"
target="_blank"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button
color="light"
fill="clear"
(click)="copy(lanAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
Your browser will warn you that the website is untrusted. You
can bypass this warning on most browsers. The warning will go
away after you
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
class="inline"
>
follow the instructions
<ion-icon name="open-outline"></ion-icon>
</a>
to downlaod and trust your Embassy's Root Certificate Authority.
</p>
<ion-button style="margin-top: 24px" (click)="installCert()">
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<br />
<div class="line"></div>
<!-- Tor Instructions -->
<h1><b>On The Go (Tor)</b></h1>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ torAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
This address will only work from a
<a
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
class="inline"
>
Tor-enabled browser
<ion-icon name="open-outline"></ion-icon> </a
>.
</p>
</div>
</ion-card-content>
</ion-card>

View File

@@ -4,26 +4,12 @@ p {
a {
text-decoration: none;
}
.toggle-label {
padding: 24px 0 8px 0;
font-weight: bold;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
* {
display: inline-block;
vertical-align: middle;
}
h2 {
font-weight: bold;
}
ion-icon {
text-align: right;
font-size: 24px;
}
}
.line {
margin-bottom: 48px;
padding-bottom: 48px;
border-bottom: solid 1px;
}

View File

@@ -16,8 +16,6 @@ import { StateService } from 'src/app/services/state.service'
})
export class SuccessPage {
@Output() onDownload = new EventEmitter()
torOpen = false
lanOpen = false
constructor(
@Inject(DOCUMENT) private readonly document: Document,
@@ -69,14 +67,6 @@ export class SuccessPage {
await toast.present()
}
toggleTor() {
this.torOpen = !this.torOpen
}
toggleLan() {
this.lanOpen = !this.lanOpen
}
installCert() {
this.document.getElementById('install-cert')?.click()
}

View File

@@ -1,20 +1,19 @@
export abstract class ApiService {
// unencrypted
abstract getStatus(): Promise<GetStatusRes> // setup.status
abstract getSecret(): Promise<string> // setup.get-secret
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
// encrypted
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
}
export type GetStatusRes = {
'product-key': boolean
migrating: boolean
}

View File

@@ -1,5 +1,10 @@
import { Injectable } from '@angular/core'
import { HttpService } from '@start9labs/shared'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
@@ -13,43 +18,71 @@ import {
SetupEmbassyRes,
} from './api.service'
import { RPCEncryptedService } from '../rpc-encrypted.service'
import * as jose from 'node-jose'
@Injectable({
providedIn: 'root',
})
export class LiveApiService extends ApiService {
export class LiveApiService implements ApiService {
constructor(
private readonly unencrypted: HttpService,
private readonly encrypted: RPCEncryptedService,
) {
super()
}
) {}
// ** UNENCRYPTED **
async getStatus() {
return this.unencrypted.rpcRequest<GetStatusRes>({
return this.rpcRequest<GetStatusRes>({
method: 'setup.status',
params: {},
})
}
/**
* We want to update the secret, which means that we will call in clearnet the
* getSecret, and all the information is never in the clear, and only public
* information is sent across the network. We don't want to expose that we do
* this wil all public/private key, which means that there is no information loss
* through the network.
*/
async getSecret() {
const keystore = jose.JWK.createKeyStore()
const key = await keystore.generate('EC', 'P-256')
// const { privateKey, publicKey } =
// jose.generateKeyPair('ECDH-ES', {
// extractable: true,
// })
console.log({ publicKey: key.toJSON() })
const response: string = await this.rpcRequest({
method: 'setup.get-secret',
params: { pubkey: key.toJSON() },
})
// const { plaintext } = await jose.compactDecrypt(response, privateKey)
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
const decoded = new TextDecoder().decode(decrypted.plaintext)
console.log({ decoded })
return decoded
}
async getDrives() {
return this.unencrypted.rpcRequest<DiskListResponse>({
return this.rpcRequest<DiskListResponse>({
method: 'setup.disk.list',
params: {},
})
}
async set02XDrive(logicalname: string) {
return this.unencrypted.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'setup.recovery.v2.set',
params: { logicalname },
})
}
async getRecoveryStatus() {
return this.unencrypted.rpcRequest<RecoveryStatusRes>({
return this.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: {},
})
@@ -65,13 +98,6 @@ export class LiveApiService extends ApiService {
})
}
async verifyProductKey() {
return this.encrypted.rpcRequest<void>({
method: 'echo',
params: { message: 'hello' },
})
}
async importDrive(params: ImportDriveReq) {
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
method: 'setup.attach',
@@ -113,6 +139,18 @@ export class LiveApiService extends ApiService {
'root-ca': btoa(res['root-ca']),
}
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.unencrypted.rpcRequest<T>(opts)
const rpcRes = res.body
if (isRpcError(rpcRes)) {
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
}
function isCifsSource(

View File

@@ -12,21 +12,29 @@ let tries = 0
@Injectable({
providedIn: 'root',
})
export class MockApiService extends ApiService {
constructor() {
super()
}
export class MockApiService implements ApiService {
// ** UNENCRYPTED **
async getStatus() {
await pauseFor(1000)
return {
'product-key': true,
migrating: false,
}
}
async getSecret() {
await pauseFor(1000)
const ascii = 'thisisasecret'
const arr1 = []
for (let n = 0, l = ascii.length; n < l; n++) {
var hex = Number(ascii.charCodeAt(n)).toString(16)
arr1.push(hex)
}
return arr1.join('')
}
async getDrives() {
await pauseFor(1000)
return {
@@ -84,11 +92,6 @@ export class MockApiService extends ApiService {
}
}
async verifyProductKey() {
await pauseFor(1000)
return
}
async importDrive(params: ImportDriveReq) {
await pauseFor(3000)
return setupRes

View File

@@ -15,13 +15,13 @@ import {
providedIn: 'root',
})
export class RPCEncryptedService {
productKey?: string
secret?: string
constructor(private readonly http: HttpService) {}
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
const encryptedBody = await AES_CTR.encryptPbkdf2(
this.productKey || '',
this.secret || '',
encodeUtf8(JSON.stringify(opts)),
)
@@ -36,7 +36,11 @@ export class RPCEncryptedService {
'Content-Type': 'application/json',
},
})
.then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body))
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
.then(x => {
console.log(`Network: ${x}`)
return x
})
.then(res => JSON.parse(res))
.catch(e => {
if (!e.status && !e.statusText) {

View File

@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
providedIn: 'root',
})
export class StateService {
hasProductKey = false
isMigrating = false
polling = false
embassyLoaded = false