Skip to content

Commit

Permalink
memoize me, my userKeys and my browserKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed May 30, 2024
1 parent dc27428 commit ec5488e
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 102 deletions.
80 changes: 80 additions & 0 deletions frontend/src/common/userdata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { base64 } from 'rfc4648';
import backend, { DeviceDto, UserDto } from './backend';
import { BrowserKeys, UserKeys } from './crypto';

class UserData {

#me?: Promise<UserDto>;
#browserKeys?: Promise<BrowserKeys | undefined>;

public get me(): Promise<UserDto> {
if (!this.#me) {
this.#me = backend.users.me(true);
}
return this.#me;
}

public get browserKeys(): Promise<BrowserKeys | undefined> {
return this.me.then(me => {
if (!this.#browserKeys) {
this.#browserKeys = BrowserKeys.load(me.id);
}
return this.#browserKeys;
});
}

public get browser(): Promise<DeviceDto | undefined> {
return this.me.then(async me => {
const browserKeys = await this.browserKeys;
if (browserKeys == null) {
return undefined;
} else {
const browserId = await browserKeys.id();
return me.devices.find(d => d.id == browserId);
}
});
}

public async reload() {
this.#me = backend.users.me(true);
this.#browserKeys = undefined;
}

public async createBrowserKeys(): Promise<BrowserKeys> {
const me = await this.me;
const browserKeys = await BrowserKeys.create();
await browserKeys.store(me.id);
this.#browserKeys = Promise.resolve(browserKeys);
return browserKeys;
}

public async decryptUserKeysWithSetupCode(setupCode: string): Promise<UserKeys> {
const me = await this.me;
if (!me.privateKey || !me.ecdhPublicKey) {
throw new Error('User not initialized.');
}
const ecdhPublicKey = base64.parse(me.ecdhPublicKey);
const ecdsaPublicKey = me.ecdsaPublicKey ? base64.parse(me.ecdsaPublicKey) : undefined; // TODO keep ecdsa key optional?
return await UserKeys.recover(me.privateKey, setupCode, ecdhPublicKey, ecdsaPublicKey);
}

public async decryptUserKeysWithBrowser(): Promise<UserKeys> {
const me = await this.me;
if (!me.privateKey || !me.ecdhPublicKey || !me.ecdsaPublicKey) {
throw new Error('User not initialized.');
}
const browserKeys = await this.browserKeys;
if (!browserKeys) {
throw new Error('Browser keys not found.');
}
const browser = await this.browser;
if (!browser) {
throw new Error('Device not initialized.');
}
return await UserKeys.decryptOnBrowser(browser.userPrivateKey, browserKeys.keyPair.privateKey, base64.parse(me.ecdhPublicKey), base64.parse(me.ecdsaPublicKey));
}

}

const instance = new UserData();
export default instance;
5 changes: 3 additions & 2 deletions frontend/src/components/AuthenticatedMain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto } from '../common/backend';
import { UserDto } from '../common/backend';
import userdata from '../common/userdata';
import FetchError from './FetchError.vue';
import NavigationBar from './NavigationBar.vue';
Expand All @@ -35,7 +36,7 @@ onMounted(fetchData);
async function fetchData() {
onFetchError.value = null;
try {
me.value = await backend.users.me();
me.value = await userdata.me;
} catch (error) {
console.error('Retrieving logged in user failed.', error);
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');
Expand Down
16 changes: 4 additions & 12 deletions frontend/src/components/DeviceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import { ComputerDesktopIcon, DevicePhoneMobileIcon, WindowIcon } from '@heroico
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { DeviceDto, NotFoundError, UserDto } from '../common/backend';
import { BrowserKeys } from '../common/crypto';
import userdata from '../common/userdata';
import FetchError from './FetchError.vue';
const { t, d } = useI18n({ useScope: 'global' });
Expand All @@ -97,32 +97,24 @@ const onRemoveDeviceError = ref< {[id: string]: Error} >({});
onMounted(async () => {
await fetchData();
await determineMyDevice();
});
async function fetchData() {
onFetchError.value = null;
try {
me.value = await backend.users.me(true);
me.value = await userdata.me;
myDevice.value = await userdata.browser;
} catch (error) {
console.error('Retrieving device list failed.', error);
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');
}
}
async function determineMyDevice() {
if (me.value == null) {
throw new Error('User not initialized.');
}
const browserKeys = await BrowserKeys.load(me.value.id);
const browserId = await browserKeys?.id();
myDevice.value = me.value.devices.find(d => d.id == browserId);
}
async function removeDevice(device: DeviceDto) {
delete onRemoveDeviceError.value[device.id];
try {
await backend.devices.removeDevice(device.id);
userdata.reload();
} catch (error) {
console.error('Removing device failed.', error);
if (error instanceof NotFoundError) {
Expand Down
45 changes: 16 additions & 29 deletions frontend/src/components/InitialSetup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,12 @@
import { ClipboardIcon } from '@heroicons/vue/20/solid';
import { CheckIcon, PencilIcon } from '@heroicons/vue/24/outline';
import { ComputerDesktopIcon, KeyIcon, ListBulletIcon } from '@heroicons/vue/24/solid';
import { base64 } from 'rfc4648';
import { nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto } from '../common/backend';
import { BrowserKeys, UnwrapKeyError, UserKeys } from '../common/crypto';
import { JWEBuilder } from '../common/jwe';
import userdata from '../common/userdata';
import { debounce } from '../common/util';
import router from '../router';
import FetchError from './FetchError.vue';
Expand Down Expand Up @@ -239,13 +239,11 @@ onMounted(fetchData);
async function fetchData() {
onFetchError.value = null;
try {
me.value = await backend.users.me(true);
const browserKeys = await BrowserKeys.load(me.value.id);
const browserId = await browserKeys?.id();
if (!me.value.ecdhPublicKey) {
me.value = await userdata.me;
if (!me.value.setupCode) {
setupCode.value = crypto.randomUUID();
state.value = State.CreateUserKey;
} else if (me.value.devices.find(d => d.id === browserId) == null) {
} else if (!await userdata.browser) {
state.value = State.RecoverUserKey;
} else {
state.value = State.SetupAlreadyCompleted;
Expand All @@ -259,18 +257,16 @@ async function fetchData() {
async function createUserKey() {
onCreateError.value = null;
try {
if (!me.value) {
throw new Error('Invalid state');
}
processing.value = true;
const me = await userdata.me;
const userKeys = await UserKeys.create();
me.value.ecdhPublicKey = await userKeys.encodedEcdhPublicKey();
me.value.ecdsaPublicKey = await userKeys.encodedEcdsaPublicKey();
me.value.privateKey = await userKeys.encryptWithSetupCode(setupCode.value);
me.value.setupCode = await JWEBuilder.ecdhEs(userKeys.ecdhKeyPair.publicKey).encrypt({ setupCode: setupCode.value });
const browserKeys = await createBrowserKeys(me.value.id);
await submitBrowserKeys(browserKeys, me.value, userKeys);
me.ecdhPublicKey = await userKeys.encodedEcdhPublicKey();
me.ecdsaPublicKey = await userKeys.encodedEcdsaPublicKey();
me.privateKey = await userKeys.encryptWithSetupCode(setupCode.value);
me.setupCode = await JWEBuilder.ecdhEs(userKeys.ecdhKeyPair.publicKey).encrypt({ setupCode: setupCode.value });
const browserKeys = await userdata.createBrowserKeys();
await submitBrowserKeys(browserKeys, me, userKeys);
await router.push('/app/vaults');
} catch (error) {
Expand All @@ -284,16 +280,12 @@ async function createUserKey() {
async function recoverUserKey() {
onRecoverError.value = null;
try {
if (!me.value || !me.value.ecdhPublicKey || !me.value.ecdsaPublicKey || !me.value.privateKey) {
throw new Error('Invalid state');
}
processing.value = true;
const ecdhPublicKey = base64.parse(me.value.ecdhPublicKey);
const ecdsaPublicKey = me.value.ecdsaPublicKey ? base64.parse(me.value.ecdsaPublicKey) : undefined;
const userKeys = await UserKeys.recover(me.value.privateKey, setupCode.value, ecdhPublicKey, ecdsaPublicKey);
const browserKeys = await createBrowserKeys(me.value.id);
await submitBrowserKeys(browserKeys, me.value, userKeys);
const me = await userdata.me;
const userKeys = await userdata.decryptUserKeysWithSetupCode(setupCode.value);
const browserKeys = await userdata.createBrowserKeys();
await submitBrowserKeys(browserKeys, me, userKeys);
await router.push('/app/vaults');
} catch (error) {
Expand All @@ -304,12 +296,6 @@ async function recoverUserKey() {
}
}
async function createBrowserKeys(userId: string): Promise<BrowserKeys> {
const browserKeys = await BrowserKeys.create();
await browserKeys.store(userId);
return browserKeys;
}
async function submitBrowserKeys(browserKeys: BrowserKeys, me: UserDto, userKeys: UserKeys) {
const jwe = await userKeys.encryptForDevice(browserKeys.keyPair.publicKey);
await backend.devices.putDevice({
Expand All @@ -321,6 +307,7 @@ async function submitBrowserKeys(browserKeys: BrowserKeys, me: UserDto, userKeys
creationTime: new Date()
});
await backend.users.putMe(me);
userdata.reload();
}
function guessBrowserName(): string {
Expand Down
21 changes: 5 additions & 16 deletions frontend/src/components/ManageSetupCode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,10 @@

<script setup lang="ts">
import { ClipboardIcon, EyeIcon, EyeSlashIcon } from '@heroicons/vue/20/solid';
import { base64 } from 'rfc4648';
import { nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend from '../common/backend';
import { BrowserKeys, UserKeys } from '../common/crypto';
import { JWEParser } from '../common/jwe';
import userdata from '../common/userdata';
import { debounce } from '../common/util';
import FetchError from './FetchError.vue';
import RegenerateSetupCodeDialog from './RegenerateSetupCodeDialog.vue';
Expand All @@ -70,20 +68,11 @@ onMounted(fetchData);
async function fetchData() {
onFetchError.value = null;
try {
const me = await backend.users.me(true);
if (me.ecdhPublicKey == null || me.ecdsaPublicKey == null || me.setupCode == null) {
throw new Error('User not initialized.');
const me = await userdata.me;
if (!me.setupCode) {
throw new Error('Invalid state');
}
const browserKeys = await BrowserKeys.load(me.id);
if (browserKeys == null) {
throw new Error('Browser keys not found.');
}
const browserId = await browserKeys.id();
const myDevice = me.devices.find(d => d.id == browserId);
if (myDevice == null) {
throw new Error('Device not initialized.');
}
const userKeys = await UserKeys.decryptOnBrowser(myDevice.userPrivateKey, browserKeys.keyPair.privateKey, base64.parse(me.ecdhPublicKey), base64.parse(me.ecdsaPublicKey));
const userKeys = await userdata.decryptUserKeysWithBrowser();
const payload : { setupCode: string } = await JWEParser.parse(me.setupCode).decryptEcdhEs(userKeys.ecdhKeyPair.privateKey);
setupCode.value = payload.setupCode;
} catch (error) {
Expand Down
19 changes: 3 additions & 16 deletions frontend/src/components/RegenerateSetupCodeDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,11 @@
import { Dialog, DialogOverlay, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { ClipboardIcon } from '@heroicons/vue/20/solid';
import { ArrowPathIcon, KeyIcon } from '@heroicons/vue/24/outline';
import { base64 } from 'rfc4648';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend from '../common/backend';
import { BrowserKeys, UserKeys } from '../common/crypto';
import { JWEBuilder } from '../common/jwe';
import userdata from '../common/userdata';
import { debounce } from '../common/util';
enum State {
Expand Down Expand Up @@ -151,21 +150,9 @@ async function regenerateSetupCode() {
try {
processing.value = true;
const me = await backend.users.me(true);
if (me.ecdhPublicKey == null || me.ecdsaPublicKey == null || me.setupCode == null) {
throw new Error('User not initialized.');
}
const browserKeys = await BrowserKeys.load(me.id);
if (browserKeys == null) {
throw new Error('Browser keys not found.');
}
const browserId = await browserKeys.id();
const myDevice = me.devices.find(d => d.id == browserId);
if (myDevice == null) {
throw new Error('Device not initialized.');
}
const me = await userdata.me;
const newCode = crypto.randomUUID();
const userKeys = await UserKeys.decryptOnBrowser(myDevice.userPrivateKey, browserKeys.keyPair.privateKey, base64.parse(me.ecdhPublicKey), base64.parse(me.ecdsaPublicKey));
const userKeys = await userdata.decryptUserKeysWithBrowser();
me.privateKey = await userKeys.encryptWithSetupCode(newCode);
me.setupCode = await JWEBuilder.ecdhEs(userKeys.ecdhKeyPair.publicKey).encrypt({ setupCode: newCode });
await backend.users.putMe(me);
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/UnlockSuccess.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<NavigationBar v-if="accountState == AccountState.Ready && browserKeys" :me="me!"/>
<NavigationBar v-if="accountState == AccountState.Ready && hasBrowserKeys" :me="me!"/>
<SimpleNavigationBar v-else-if="me" :me="me"/>

<div class="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8 flex justify-center">
Expand Down Expand Up @@ -38,7 +38,7 @@
<p class="my-3">
Please enter your account key in Cryptomator to authorize it.
</p>
<router-link v-if="browserKeys" to="/app/profile" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">View Account Key in my Profile</router-link>
<router-link v-if="hasBrowserKeys" to="/app/profile" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">View Account Key in my Profile</router-link>
</div>

<!-- NO VAULT ACCESS -->
Expand Down Expand Up @@ -68,7 +68,7 @@
import { ComputedRef, computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto, VaultDto } from '../common/backend';
import { BrowserKeys } from '../common/crypto';
import userdata from '../common/userdata';
import FetchError from './FetchError.vue';
import NavigationBar from './NavigationBar.vue';
import SimpleNavigationBar from './SimpleNavigationBar.vue';
Expand Down Expand Up @@ -119,7 +119,7 @@ enum VaultAccess {
}
const me = ref<UserDto>();
const browserKeys = ref<boolean>(false);
const hasBrowserKeys = ref<boolean>(false);
const accessibleVaults = ref<VaultDto[]>();
const onFetchError = ref<Error | null>();
Expand All @@ -128,8 +128,8 @@ onMounted(fetchData);
async function fetchData() {
onFetchError.value = null;
try {
me.value = await backend.users.me(true);
browserKeys.value = await BrowserKeys.load(me.value.id) != null;
me.value = await userdata.me;
hasBrowserKeys.value = await userdata.browserKeys.then(keys => keys !== undefined);
accessibleVaults.value = await backend.vaults.listAccessible();
} catch (error) {
console.error('Retrieving user information failed.', error);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/UserProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { useI18n } from 'vue-i18n';
import backend, { UserDto, VersionDto } from '../common/backend';
import config from '../common/config';
import userdata from '../common/userdata';
import { Locale } from '../i18n';
import DeviceList from './DeviceList.vue';
import FetchError from './FetchError.vue';
Expand All @@ -90,7 +91,7 @@ onMounted(async () => {
async function fetchData() {
onFetchError.value = null;
try {
me.value = await backend.users.me(true);
me.value = await userdata.me;
version.value = await backend.version.get();
} catch (error) {
console.error('Retrieving user information failed.', error);
Expand Down
Loading

0 comments on commit ec5488e

Please sign in to comment.