Skip to content

Commit

Permalink
clean up and deduplicate
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Jun 1, 2024
1 parent e757bdd commit a4aa396
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 32 deletions.
73 changes: 58 additions & 15 deletions frontend/src/common/userdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ class UserData {
#me?: Promise<UserDto>;
#browserKeys?: Promise<BrowserKeys | undefined>;

/**
* Gets the user DTO representing the currently logged in user.
*/
public get me(): Promise<UserDto> {
if (!this.#me) {
this.#me = backend.users.me(true);
}
return this.#me;
}

/**
* Gets the device key pair stored for this user in the currently used browser.
*/
public get browserKeys(): Promise<BrowserKeys | undefined> {
return this.me.then(me => {
if (!this.#browserKeys) {
Expand All @@ -24,23 +30,59 @@ class UserData {
});
}

/**
* Gets the device that represents the currently used browser.
*/
public get browser(): Promise<DeviceDto | undefined> {
return this.me.then(async me => {
const browserKeys = await this.browserKeys;
if (browserKeys == null) {
return undefined;
const browserId = await browserKeys?.id();
return browserId ? me.devices.find(d => d.id === browserId) : undefined;
});
}

/**
* Gets the ECDH public key of the user.
*
* @see UserDto.ecdhPublicKey
*/
public get ecdhPublicKey(): Promise<Uint8Array> {
return this.me.then(me => {
if (!me.ecdhPublicKey) {
throw new Error('User not initialized.');
}
return base64.parse(me.ecdhPublicKey);
});
}

/**
* Gets the ECDSA public key of the user, if available.
*
* @see UserDto.ecdsaPublicKey
*/
public get ecdsaPublicKey(): Promise<Uint8Array | undefined> {
return this.me.then(me => {
if (me.ecdsaPublicKey) {
return base64.parse(me.ecdsaPublicKey);
} else {
const browserId = await browserKeys.id();
return me.devices.find(d => d.id === browserId);
return undefined;
}
});
}

/**
* Invalidates the cached user data and reloads it in the backend.
*/
public async reload() {
this.#me = backend.users.me(true);
this.#browserKeys = undefined;
}

/**
* Creates a new browser key pair for the user.
* This does not change the device DTO stored in the backend.
* @returns A new browser key pair for the user.
*/
public async createBrowserKeys(): Promise<BrowserKeys> {
const me = await this.me;
const browserKeys = await BrowserKeys.create();
Expand All @@ -49,23 +91,26 @@ class UserData {
return browserKeys;
}

/**
* Decrypts the user keys using the setup code.
* @param setupCode the setup code
* @returns The user's key pairs
*/
public async decryptUserKeysWithSetupCode(setupCode: string): Promise<UserKeys> {
const me = await this.me;
if (!me.privateKey || !me.ecdhPublicKey) {
if (!me.privateKey) {
throw new Error('User not initialized.');
}
const ecdhPublicKey = base64.parse(me.ecdhPublicKey);
const ecdsaPublicKey = me.ecdsaPublicKey ? base64.parse(me.ecdsaPublicKey) : undefined;
const userKeys = await UserKeys.recover(me.privateKey, setupCode, ecdhPublicKey, ecdsaPublicKey);
const userKeys = await UserKeys.recover(me.privateKey, setupCode, await this.ecdhPublicKey, await this.ecdsaPublicKey);
await this.addEcdsaKeyIfMissing(userKeys);
return userKeys;
}

/**
* Decrypts the user keys using the device key stored in the currently used browser.
* @returns The user's key pairs
*/
public async decryptUserKeysWithBrowser(): Promise<UserKeys> {
const me = await this.me;
if (!me.ecdhPublicKey) {
throw new Error('User not initialized.');
}
const browserKeys = await this.browserKeys;
if (!browserKeys) {
throw new Error('Browser keys not found.');
Expand All @@ -74,9 +119,7 @@ class UserData {
if (!browser) {
throw new Error('Device not initialized.');
}
const ecdhPublicKey = base64.parse(me.ecdhPublicKey);
const ecdsaPublicKey = me.ecdsaPublicKey ? base64.parse(me.ecdsaPublicKey) : undefined;
const userKeys = await UserKeys.decryptOnBrowser(browser.userPrivateKey, browserKeys.keyPair.privateKey, ecdhPublicKey, ecdsaPublicKey);
const userKeys = await UserKeys.decryptOnBrowser(browser.userPrivateKey, browserKeys.keyPair.privateKey, await this.ecdhPublicKey, await this.ecdsaPublicKey);
await this.addEcdsaKeyIfMissing(userKeys);
return userKeys;
}
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/components/CreateVault.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ import { ClipboardIcon } from '@heroicons/vue/20/solid';
import { ArrowPathIcon, CheckIcon, KeyIcon } from '@heroicons/vue/24/outline';
import { ArrowDownTrayIcon } from '@heroicons/vue/24/solid';
import { saveAs } from 'file-saver';
import { base64 } from 'rfc4648';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { PaymentRequiredError } from '../common/backend';
Expand Down Expand Up @@ -290,12 +289,9 @@ async function createVault() {
}
processing.value = true;
const owner = await userdata.me;
if (!owner.ecdhPublicKey) {
throw new Error('Invalid state');
}
const vaultId = crypto.randomUUID();
vaultConfig.value = await VaultConfig.create(vaultId, vaultKeys.value);
const ownerJwe = await vaultKeys.value.encryptForUser(base64.parse(owner.ecdhPublicKey));
const ownerJwe = await vaultKeys.value.encryptForUser(await userdata.ecdhPublicKey);
await backend.vaults.createOrUpdateVault(vaultId, vaultName.value, false, vaultDescription.value);
await backend.vaults.grantAccess(vaultId, { userId: owner.id, token: ownerJwe });
state.value = State.Finished;
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/components/RecoverVaultDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@

<script setup lang="ts">
import { Dialog, DialogOverlay, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { base64 } from 'rfc4648';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto, VaultDto } from '../common/backend';
import backend, { VaultDto } from '../common/backend';
import { VaultKeys } from '../common/crypto';
import userdata from '../common/userdata';
class FormValidationFailedError extends Error {
constructor() {
Expand All @@ -76,8 +76,7 @@ const recoveryKey = ref('');
const processingVaultRecovery = ref(false);
const props = defineProps<{
vault: VaultDto,
me: UserDto
vault: VaultDto
}>();
const emit = defineEmits<{
Expand Down Expand Up @@ -105,10 +104,11 @@ async function recoverVault() {
try {
processingVaultRecovery.value = true;
const vaultKeys = await VaultKeys.recover(recoveryKey.value);
if (props.me.ecdhPublicKey && vaultKeys) {
const publicKey = base64.parse(props.me.ecdhPublicKey);
const me = await userdata.me;
if (vaultKeys) {
const publicKey = await userdata.ecdhPublicKey;
const jwe = await vaultKeys.encryptForUser(publicKey);
await backend.vaults.grantAccess(props.vault.id, { userId: props.me.id, token: jwe });
await backend.vaults.grantAccess(props.vault.id, { userId: me.id, token: jwe });
emit('recovered');
open.value = false;
}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/VaultDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,13 @@
<DisplayRecoveryKeyDialog v-if="displayingRecoveryKey && vault && vaultKeys" ref="displayRecoveryKeyDialog" :vault="vault" :vault-keys="vaultKeys" @close="displayingRecoveryKey = false" />
<ArchiveVaultDialog v-if="archivingVault && vault" ref="archiveVaultDialog" :vault="vault" @close="archivingVault = false" @archived="v => refreshVault(v)" />
<ReactivateVaultDialog v-if="reactivatingVault && vault" ref="reactivateVaultDialog" :vault="vault" @close="reactivatingVault = false" @reactivated="v => { refreshVault(v); refreshLicense();}" />
<RecoverVaultDialog v-if="recoveringVault && vault && me" ref="recoverVaultDialog" :vault="vault" :me="me" @close="recoveringVault = false" @recovered="fetchOwnerData()" />
<RecoverVaultDialog v-if="recoveringVault && vault" ref="recoverVaultDialog" :vault="vault" @close="recoveringVault = false" @recovered="fetchOwnerData()" />
</template>

<script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import { ArrowPathIcon, EllipsisVerticalIcon, ExclamationTriangleIcon } from '@heroicons/vue/20/solid';
import { PlusSmallIcon } from '@heroicons/vue/24/solid';
import { base64 } from 'rfc4648';
import { computed, nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import auth from '../common/auth';
Expand Down Expand Up @@ -320,8 +319,8 @@ async function loadVaultKeys(vaultKeyJwe: string): Promise<VaultKeys> {
}
async function provedOwnership(keys: VaultKeys, ownerKeyPair: CryptoKeyPair) {
if (!me.value || !me.value.ecdhPublicKey) {
throw new Error('User not initialized.');
if (!me.value) {
throw new Error('illegal state');
}
const header: JWTHeader = { alg: 'ES384', typ: 'JWT', b64: true };
Expand All @@ -336,7 +335,8 @@ async function provedOwnership(keys: VaultKeys, ownerKeyPair: CryptoKeyPair) {
return;
}
const vaultKeyJwe = keys.encryptForUser(base64.parse(me.value.ecdhPublicKey));
const myPublicKey = await userdata.ecdhPublicKey;
const vaultKeyJwe = keys.encryptForUser(myPublicKey);
try {
await backend.vaults.grantAccess(props.vaultId, { userId: me.value.id, token: await vaultKeyJwe });
} catch (error) {
Expand Down

0 comments on commit a4aa396

Please sign in to comment.