Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic to deal with interruptions at intermediate states during key generation and setup #225

Merged
merged 4 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ contextMenu({
click: () => (browserWindow as BrowserWindow).reload(),
},
{
label: 'Quit',
label: 'Quit Launcher',
click: () => app.quit(),
},
],
Expand Down Expand Up @@ -391,20 +391,15 @@ const handleSignZomeCall = async (e: IpcMainInvokeEvent, request: CallZomeReques
};

// Kills any pre-existing lair processes and start up lair and assigns associated global variables
async function killIfExistsAndLaunchLair(password: string): Promise<void> {
async function launchLairIfNecessary(password: string): Promise<void> {
INTEGRITY_CHECKER = new IntegrityChecker(password);
LAUNCHER_FILE_SYSTEM.setIntegrityChecker(INTEGRITY_CHECKER);
LAUNCHER_EMITTER.emit(LOADING_PROGRESS_UPDATE, 'startingLairKeystore');

if (LAIR_HANDLE) {
LAIR_HANDLE.kill();
LAIR_HANDLE = undefined;
}

if (VALIDATED_CLI_ARGS.holochainVersion.type === 'running-external') {
LAIR_URL = VALIDATED_CLI_ARGS.holochainVersion.lairUrl;
DEFAULT_LAIR_CLIENT = await rustUtils.LauncherLairClient.connect(LAIR_URL, password);
} else {
} else if (!LAIR_HANDLE) {
const [lairHandle, lairUrl2] = await launchLairKeystore(
VALIDATED_CLI_ARGS.lairBinaryPath,
LAUNCHER_FILE_SYSTEM.keystoreDir,
Expand Down Expand Up @@ -444,7 +439,7 @@ async function handleQuickSetup(password: string): Promise<void> {
}

// 2. Start up lair keystore process and connect to it
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);

// 3. Generate key recovery file and import device seed if necessary
const dpkiDeviceSeedExists = await DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
Expand Down Expand Up @@ -495,7 +490,7 @@ async function handleAdvancedSetup(
}

// 2. Start up lair keystore process and connect to it
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);

// 3. Make sure that there is no DPKI_DEVICE_SEED in lair yet
const dpkiDeviceSeedExists = await DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
Expand Down Expand Up @@ -882,6 +877,15 @@ const router = t.router({

return !isInitializedValidated;
}),
dpkiDeviceSeedPresent: t.procedure
.input(z.object({ password: z.string() }))
.mutation(async (opts) => {
const password = opts.input.password;
if (!DEFAULT_LAIR_CLIENT || !LAIR_URL || !LAIR_HANDLE) {
await launchLairIfNecessary(password);
}
return DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
}),
getHolochainStorageInfo: t.procedure.query(() => {
const defaultHolochainManager = HOLOCHAIN_MANAGERS[BREAKING_DEFAULT_HOLOCHAIN_VERSION];
const holochainDataRoot = defaultHolochainManager.holochainDataRoot;
Expand Down Expand Up @@ -953,7 +957,7 @@ const router = t.router({
if (!PRIVILEDGED_LAUNCHER_WINDOWS)
c12i marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Main window needs to exist before launching.');
if (!DEFAULT_LAIR_CLIENT || !LAIR_URL || !LAIR_HANDLE) {
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);
}
await launchHolochain(password, LAIR_URL!);
IS_LAUNCHED = true;
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ body {
}

.btn-black {
@apply btn px-2 border border-surface-400 bg-transparent font-semibold;
@apply btn px-2 border border-surface-400 bg-[#1e1e1e] font-semibold;
}

.btn-next {
Expand Down
51 changes: 51 additions & 0 deletions src/renderer/src/lib/helpers/launcherPersistedStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import localforage from 'localforage';

/**
* This is a Class acts as a unified accessor for all things stored in
* [localforage](https://www.npmjs.com/package/localforage).
*/
export class LauncherPersistedStore {
private store: LocalforageStore;

constructor(store?: LocalforageStore) {
this.store = store ? store : new LocalforageStore();
}

/**
* Stores the allowlist from the given URL in localStorage for it to be
* available also without internet access.
*/
allowList: SubStore<string, string, [string]> = {
value: async (url) => this.store.getItem<string>(`allowList${url}`),
set: async (value, url) => this.store.setItem<string>(`allowList${url}`, value)
};

/**
* Stores that the Quick Setup option was chosen in case that setup
* fails and the user restarts when lair keystore is already initialized.
* Launcher will directly continue with the Quick Setup steps after
* entering the password in this case.
*/
quickSetupChosen: SubStore<boolean, boolean, []> = {
value: () => this.store.getItem<boolean>('quickSetupChosen'),
set: (value) => this.store.setItem<boolean>('quickSetupChosen', value)
};
}

export interface SubStore<T, U, V extends any[]> {
value: (...args: V) => Promise<T | null>;
set: (value: U, ...args: V) => Promise<U>;
}

export class LocalforageStore {
getItem = async <T>(key: string): Promise<T | null> => localforage.getItem(key) as Promise<T>;

setItem = async <T>(key: string, value: T): Promise<T> => localforage.setItem(key, value);

removeItem = async (key: string) => localforage.removeItem(key);

clear = async () => localforage.clear();

keys = async () => localforage.keys();
}
11 changes: 6 additions & 5 deletions src/renderer/src/lib/helpers/other.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CellType, encodeHashToBase64, type CellInfo } from '@holochain/client';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CellType, encodeHashToBase64 } from '@holochain/client';
import type { AppstoreAppClient, AppVersionEntry, Entity } from 'appstore-tools';
import localforage from 'localforage';

Expand All @@ -18,7 +19,7 @@ import { AppstoreFilterListsSchema } from '$types/happs';
export function recursiveSum(obj: any): number {
let sum = 0;

for (let key in obj) {
for (const key in obj) {
if (typeof obj[key] === 'object') {
sum += recursiveSum(obj[key]);
} else {
Expand All @@ -31,8 +32,8 @@ export function recursiveSum(obj: any): number {

export function divideObject(obj: any, divider: number) {
const newObj: any = {};
for (let key in obj) {
newObj[key] = obj[key]/divider;
for (const key in obj) {
newObj[key] = obj[key] / divider;
}
return newObj;
}
Expand Down Expand Up @@ -105,7 +106,7 @@ export const base64ToArrayBuffer = (base64: string) => {

export const createImageUrl = (icon?: Uint8Array) => {
return icon ? URL.createObjectURL(new File([icon], 'icon')) : undefined;
}
};

export const initializeDefaultAppPorts = async (data: InitializeAppPorts) => {
const { appPort, appstoreAuthenticationToken, devhubAuthenticationToken } = data;
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/lib/locale/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"chooseAPassphrase": "Choose passphrase",
"choosePassword": "Choose a password to encrypt your data and private keys. You will always need this password to start Launcher.",
"city": "City",
"completeYourSetup": "Complete Your Setup",
"confirmPassphrase": "Confirm the passphrase",
"confirmPassword": "Confirm password",
"confirmYourPassphrase": "Confirm your passphrase",
Expand All @@ -38,6 +39,7 @@
"enabled": "Enabled",
"enterAppName": "Enter app name",
"enterNetworkSeed": "Enter network seed",
"enterPassword": "Enter Password",
"exportLogs": "Export Logs",
"factoryReset": "Factory Reset",
"factoryResetClick": "Click here to factory reset your Holochain Launcher",
Expand All @@ -46,6 +48,7 @@
"factoryResetHolochainLauncher": "Fully reset Holochain Launcher. This will delete all your apps alongside any associated data as well as your private keys.",
"factoryResetModalBody": "WARNING: This will delete all your apps alongside any data therein as well as your private keys.",
"fetchingAppData": "Fetching app data",
"finishAdvancedSetup": "Finish Advanced Setup",
"generatingKeyRecoveryFile": "Generating key recovery file...",
"getStarted": "Get started",
"gettingAppBytes": "Getting app bytes",
Expand Down Expand Up @@ -111,13 +114,15 @@
"region": "Region",
"releases": "Releases",
"reset": "Reset",
"restartedWithSetupIncompleteNotice": "It looks like launcher got restarted before the advanced setup process completed successfully.",
"roleName": "Role Name",
"searchForApps": "Search for apps",
"setUpYourPublisherProfile": "Set up your publisher profile",
"setLauncherPassword": "Set Launcher Password",
"settings": "Settings",
"setupError": "Setup Error",
"setupProgress": "Setup Progress",
"skipAndLaunch": "Skip and Launch (Quick Setup)",
"startingHolochain": "Starting Holochain...",
"startingLairKeystore": "Starting Lair Keystore...",
"show": "show",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/lib/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './hideUnverifiedApps';
export * from './importedKeys';
export * from './password';
export * from './setup';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { KeyFile } from 'hc-launcher-rust-utils';
import { writable, type Writable } from 'svelte/store';
import { type Writable, writable } from 'svelte/store';

export const appPassword: Writable<string> = writable('');
export const recoveryKeysPassphrase: Writable<string> = writable('');
export const generatedKeyRecoveryFile: Writable<KeyFile | undefined> = writable(undefined);
export const setupProgress: Writable<string> = writable('');
41 changes: 26 additions & 15 deletions src/renderer/src/routes/(setup)/advanced-setup-step-3/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button, IconButton } from '$components';
import { ArrowRight, BackArrow } from '$icons';
import { i18n } from '$services';
import { BackArrow, ArrowRight } from '$icons';
import { IconButton, Button } from '$components';
import { appPassword } from '$stores';

const keystoreInitialized = window.sessionStorage.getItem('keystoreInitialized');
</script>

<div class="fixed top-0 left-0 right-0 app-region-drag flex items-center justify-between p-3 bg-[#DADADA12]">
<div
class="app-region-drag fixed left-0 right-0 top-0 flex items-center justify-between bg-[#DADADA12] p-3"
c12i marked this conversation as resolved.
Show resolved Hide resolved
>
<div class="relative flex w-full items-center justify-center py-[11px]">
<IconButton buttonClass="absolute left-2" onClick={() => {
$appPassword = '';
goto('advanced-setup-step-1')
}}><BackArrow /></IconButton>
<span class="text-center text-semibold text-lg text-white">Advanced Setup (3 / 6)</span>
{#if !keystoreInitialized}
<IconButton
buttonClass="absolute left-2"
onClick={() => {
$appPassword = '';
goto('advanced-setup-step-1');
}}><BackArrow /></IconButton
>
{/if}
<span class="text-semibold text-center text-lg text-white">Advanced Setup (3 / 6)</span>
</div>
</div>


<h1 class="h1 mb-10">2. Generate Recovery Keys</h1>

<div class="p-5 pl-10 mr-20 ml-20 bg-[#3f5b91] mb-10 rounded-lg">
<ul class="list-disc text-lg text-left font-semibold">
<li>Data that you create with Holochain is signed by secret private keys stored on this device</li>
<div class="mb-10 ml-20 mr-20 rounded-lg bg-[#3f5b91] p-5 pl-10">
<ul class="list-disc text-left text-lg font-semibold">
<li>
Data that you create with Holochain is signed by secret private keys stored on this device
</li>
<li>For every Holochain app a different private key will be used</li>
<li>The following 3 steps guide you through the creation of a file containing encrypted recovery keys that allow you to regenerate the private
keys used for your apps in case that you lose access to this device</li>
<li>
The following 3 steps guide you through the creation of a file containing encrypted recovery
keys that allow you to regenerate the private keys used for your apps in case that you lose
access to this device
</li>
</ul>

</div>

<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { IconButton } from '$components';
import { BackArrow } from '$icons';
import { i18n } from '$services';
import { recoveryKeysPassphrase } from '$stores';
import { BackArrow } from '$icons';
import { IconButton } from '$components';

import { PasswordForm } from '../components';
</script>

<div class="fixed top-0 left-0 right-0 app-region-drag flex items-center justify-between p-3 bg-[#DADADA12]">
<div
class="app-region-drag fixed left-0 right-0 top-0 flex items-center justify-between bg-[#DADADA12] p-3"
>
<div class="relative flex w-full items-center justify-center py-[11px]">
<IconButton buttonClass="absolute left-2" onClick={() => goto('advanced-setup-step-3')}><BackArrow /></IconButton>
<span class="text-center text-semibold text-lg text-white">Advanced Setup (4 / 6)</span>
<IconButton buttonClass="absolute left-2" onClick={() => goto('advanced-setup-step-3')}
><BackArrow /></IconButton
>
<span class="text-semibold text-center text-lg text-white">Advanced Setup (4 / 6)</span>
</div>
</div>


<h1 class="h1 mb-10">2. Generate Recovery Keys</h1>

<h2 class="h2 mb-4 max-w-[380px]">Choose a secure passphrase to encrypt your recovery keys:</h2>
Expand Down
Loading