diff --git a/src/lib/components/Init.svelte b/src/lib/components/Init.svelte index 367c67c..3b9b390 100644 --- a/src/lib/components/Init.svelte +++ b/src/lib/components/Init.svelte @@ -3,7 +3,7 @@ import { dismissWindow } from '$lib/helpers'; function redirectToSetup() { - window.open('setup/start.html', '_blank'); + window.open('setup-pass/start.html', '_blank'); } diff --git a/src/lib/const/query-keys.ts b/src/lib/const/query-keys.ts index 74d5a56..3358e8f 100644 --- a/src/lib/const/query-keys.ts +++ b/src/lib/const/query-keys.ts @@ -1,2 +1,3 @@ export const SESSION_DATA_KEY = 'sessionDataKey'; export const SETUP_KEY = 'setupKey'; +export const SETUP_PASSWORD = 'setupPassword'; diff --git a/src/lib/const/secure-store.ts b/src/lib/const/secure-store.ts index 1acb53f..cb41a1a 100644 --- a/src/lib/const/secure-store.ts +++ b/src/lib/const/secure-store.ts @@ -2,4 +2,5 @@ export const SESSION = 'session'; export const LOCAL = 'local'; export const SESSION_DATA = 'sessionData'; -export const PASSWORD_WITH_DEVICE_KEY = 'passwordWithDeviceKey'; +export const PASSWORD = 'password'; +export const DEVICE_KEY = 'deviceKey'; diff --git a/src/lib/queries/app-queries.ts b/src/lib/queries/app-queries.ts index 3451376..10921c9 100644 --- a/src/lib/queries/app-queries.ts +++ b/src/lib/queries/app-queries.ts @@ -1,15 +1,17 @@ import { createMutation, useQueryClient, createQuery } from '@tanstack/svelte-query'; import { storageService } from '$services'; -import { PasswordAndSecureDataSchema, SessionStateSchema } from '$types'; +import { Password, SecureDataSchema, SessionStateSchema } from '$types'; import { LOCAL, - PASSWORD_WITH_DEVICE_KEY, + PASSWORD, + DEVICE_KEY, SESSION, SESSION_DATA, SESSION_DATA_KEY, - SETUP_KEY + SETUP_KEY, + SETUP_PASSWORD } from '$const'; -import { encryptData, hashPassword } from '$helpers'; +import { decryptData, encryptData, hashPassword } from '$helpers'; export function sessionStorageQueries() { const queryClient = useQueryClient(); @@ -28,16 +30,13 @@ export function sessionStorageQueries() { const signInMutation = createMutation({ mutationFn: async (password: string) => { const result = await storageService.getWithoutCallback({ - key: PASSWORD_WITH_DEVICE_KEY, + key: PASSWORD, area: LOCAL }); - const validatedResult = PasswordAndSecureDataSchema.safeParse(result); + const validatedResult = Password.safeParse(result); - if ( - validatedResult.success && - (await hashPassword(password)) === validatedResult.data.password - ) { + if (validatedResult.success && (await hashPassword(password)) === validatedResult.data) { return storageService.set({ key: SESSION_DATA, value: true, @@ -51,35 +50,125 @@ export function sessionStorageQueries() { queryClient.invalidateQueries({ queryKey: [SESSION_DATA_KEY] }); } }); - const setupQuery = createQuery({ + const setupPasswordQuery = createQuery({ + queryKey: [SETUP_PASSWORD], + queryFn: async () => { + const data = await storageService.getWithoutCallback({ + key: PASSWORD, + area: LOCAL + }); + + const validatedData = Password.safeParse(data); + return validatedData.success; + } + }); + const setupDeviceKeyQuery = createQuery({ queryKey: [SETUP_KEY], queryFn: async () => { const data = await storageService.getWithoutCallback({ - key: PASSWORD_WITH_DEVICE_KEY, + key: DEVICE_KEY, area: LOCAL }); - const validatedData = PasswordAndSecureDataSchema.safeParse(data); + const validatedData = SecureDataSchema.safeParse(data); return validatedData.success; } }); - const passwordWithDeviceKeyMutation = createMutation({ - mutationFn: async (mutationData: { password: string; secretData: Uint8Array }) => { - const hash = await hashPassword(mutationData.password); + const createPassword = createMutation({ + mutationFn: async (password: string) => { + const hash = await hashPassword(password); storageService.set({ - key: PASSWORD_WITH_DEVICE_KEY, - value: { - password: hash, - secureData: await encryptData(mutationData.secretData, hash) - }, + key: PASSWORD, + value: hash, area: LOCAL }); }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [SETUP_PASSWORD] }); + } + }); + const storeDeviceKey = createMutation({ + mutationFn: async (deviceKey: Uint8Array) => { + const result = await storageService.getWithoutCallback({ + key: PASSWORD, + area: LOCAL + }); + + const validatePassword = Password.safeParse(result); + + if (validatePassword.success) { + storageService.set({ + key: DEVICE_KEY, + value: await encryptData(deviceKey, validatePassword.data), + area: LOCAL + }); + } + }, + onSuccess: () => { queryClient.invalidateQueries({ queryKey: [SETUP_KEY] }); } }); - return { setupQuery, passwordWithDeviceKeyMutation, sessionQuery, signInMutation }; + const changePasswordWithDeviceKeyMutation = createMutation({ + mutationFn: async (mutationData: { newPassword: string; oldPassword: string }) => { + const oldHash = await hashPassword(mutationData.oldPassword); + + const result = await storageService.getWithoutCallback({ + key: PASSWORD, + area: LOCAL + }); + + const validatePassword = Password.safeParse(result); + + if (!validatePassword.success || oldHash !== validatePassword.data) { + throw new Error('Invalid password'); + } + + const deviceKey = await storageService.getWithoutCallback({ + key: DEVICE_KEY, + area: LOCAL + }); + + const validatedDeviceKey = SecureDataSchema.safeParse(deviceKey); + + if (!validatedDeviceKey.success) { + throw new Error('Invalid device key'); + } + + const decryptedData = await decryptData(validatedDeviceKey.data, oldHash); + const newHash = await hashPassword(mutationData.newPassword); + + storageService.set({ + key: PASSWORD, + value: newHash, + area: LOCAL + }); + storageService.set({ + key: DEVICE_KEY, + value: await encryptData(decryptedData, newHash), + area: LOCAL + }); + storageService.set({ + key: SESSION_DATA, + value: false, + area: SESSION + }); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [SESSION_DATA_KEY] }); + } + }); + + return { + createPassword, + setupDeviceKeyQuery, + setupPasswordQuery, + changePasswordWithDeviceKeyMutation, + sessionQuery, + signInMutation, + storeDeviceKey + }; } diff --git a/src/lib/services/storage.ts b/src/lib/services/storage.ts index 89126f1..133634e 100644 --- a/src/lib/services/storage.ts +++ b/src/lib/services/storage.ts @@ -1,19 +1,31 @@ import { isChromeStorageSafe } from '$helpers'; -import type { AreaName, ChangesType, PasswordAndSecureData } from '$types'; -import type { LOCAL, SESSION, SESSION_DATA, PASSWORD_WITH_DEVICE_KEY } from '$const'; +import type { AreaName, ChangesType, SecureData } from '$types'; +import type { LOCAL, SESSION, SESSION_DATA, PASSWORD, DEVICE_KEY } from '$const'; -type StorageItem = - | { key: typeof SESSION_DATA; value: boolean; area: typeof SESSION } - | { key: typeof PASSWORD_WITH_DEVICE_KEY; value: PasswordAndSecureData; area: typeof LOCAL }; +type SetSession = { key: typeof SESSION_DATA; value: boolean; area: typeof SESSION }; +type GetSession = { key: typeof SESSION_DATA; area: typeof SESSION }; -type GetStorageItem = - | { key: typeof SESSION_DATA; area: typeof SESSION } - | { key: typeof PASSWORD_WITH_DEVICE_KEY; area: typeof LOCAL }; +type SetPassword = { + key: typeof PASSWORD; + value: string; + area: typeof LOCAL; +}; +type GetPassword = { key: typeof PASSWORD; area: typeof LOCAL }; + +type SetDeviceKey = { + key: typeof DEVICE_KEY; + value: SecureData; + area: typeof LOCAL; +}; +type GetDeviceKey = { key: typeof DEVICE_KEY; area: typeof LOCAL }; + +type StorageSetItem = SetSession | SetPassword | SetDeviceKey; +type StorageGetItem = GetSession | GetPassword | GetDeviceKey; type StorageService = { - set: (item: StorageItem) => void; - get: (item: GetStorageItem, callback: (value: unknown) => void) => void; - getWithoutCallback: (item: GetStorageItem) => Promise; + set: (item: StorageSetItem) => void; + get: (item: StorageGetItem, callback: (value: unknown) => void) => void; + getWithoutCallback: (item: StorageGetItem) => Promise; addListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => void; removeListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => void; }; diff --git a/src/lib/stores/keys-store.ts b/src/lib/stores/keys-store.ts index 2d42244..5949275 100644 --- a/src/lib/stores/keys-store.ts +++ b/src/lib/stores/keys-store.ts @@ -27,15 +27,6 @@ const initKeysStore = () => { set(initialState); } }, - resetExceptDevice: () => - update((state) => ({ - ...initialState, - keys: { - ...state.keys, - master: null, - revocation: null - } - })), resetAll: () => update(() => initialState) }; }; diff --git a/src/lib/types/keys.ts b/src/lib/types/keys.ts index 0cb03a0..bf40fe1 100644 --- a/src/lib/types/keys.ts +++ b/src/lib/types/keys.ts @@ -21,10 +21,3 @@ export const SecureDataSchema = z.object({ }); export type SecureData = z.infer; - -export const PasswordAndSecureDataSchema = z.object({ - password: Password, - secureData: SecureDataSchema -}); - -export type PasswordAndSecureData = z.infer; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5773084..355be9e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,10 +3,14 @@ import { dismissWindow } from '$helpers'; import { sessionStorageQueries } from '$queries'; - const { sessionQuery, setupQuery } = sessionStorageQueries(); + const { sessionQuery, setupDeviceKeyQuery } = sessionStorageQueries(); + + function redirectToChangePassword() { + window.open('change-password.html', '_blank'); + } -{#if $sessionQuery.isFetching || $setupQuery.isFetching} +{#if $sessionQuery.isFetching || $setupDeviceKeyQuery.isFetching} Loading {:else if $sessionQuery.data}
@@ -17,8 +21,11 @@

Holo Key Manager

+ -{:else if $setupQuery.data} +{:else if $setupDeviceKeyQuery.data} {:else} diff --git a/src/routes/change-password/+page.svelte b/src/routes/change-password/+page.svelte new file mode 100644 index 0000000..2c27fef --- /dev/null +++ b/src/routes/change-password/+page.svelte @@ -0,0 +1,62 @@ + + + + + Manage Password + {#if $changePasswordWithDeviceKeyMutation.error} + {$changePasswordWithDeviceKeyMutation.error.message} + {/if} +
+ + + +
+ +
+
+
diff --git a/src/routes/done/+page.svelte b/src/routes/done/+page.svelte deleted file mode 100644 index dae3235..0000000 --- a/src/routes/done/+page.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - Holo Key Manager Logo - Setup Complete -
- - -
-
diff --git a/src/routes/setup-keys/+layout.svelte b/src/routes/setup-keys/+layout.svelte new file mode 100644 index 0000000..a6c06dd --- /dev/null +++ b/src/routes/setup-keys/+layout.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/src/routes/setup-keys/done/+page.svelte b/src/routes/setup-keys/done/+page.svelte new file mode 100644 index 0000000..da084a3 --- /dev/null +++ b/src/routes/setup-keys/done/+page.svelte @@ -0,0 +1,16 @@ + + +Holo Key Manager Logo +Setup Complete +
+ + +
diff --git a/src/routes/setup/download/+page.svelte b/src/routes/setup-keys/download/+page.svelte similarity index 94% rename from src/routes/setup/download/+page.svelte rename to src/routes/setup-keys/download/+page.svelte index d447104..13ee8aa 100644 --- a/src/routes/setup/download/+page.svelte +++ b/src/routes/setup-keys/download/+page.svelte @@ -12,7 +12,7 @@ $keysStore.keys.revocation === null || $keysStore.keys.device === null ) { - goto('/setup/start'); + goto('/setup-pass/start'); } }); @@ -40,9 +40,9 @@ saveAs(content, 'keys.zip'); - keysStore.resetExceptDevice(); + keysStore.resetAll(); - goto('/setup/app-password'); + goto('done'); } } diff --git a/src/routes/setup/generate-keys/+page.svelte b/src/routes/setup-keys/generate-keys/+page.svelte similarity index 81% rename from src/routes/setup/generate-keys/+page.svelte rename to src/routes/setup-keys/generate-keys/+page.svelte index 08ef9ef..6eed9be 100644 --- a/src/routes/setup/generate-keys/+page.svelte +++ b/src/routes/setup-keys/generate-keys/+page.svelte @@ -3,10 +3,13 @@ import { keysStore, passphraseStore } from '$stores'; import { goto } from '$app/navigation'; import { Button, Title, AppParagraph } from '$components'; + import { sessionStorageQueries } from '$queries'; + + const { storeDeviceKey } = sessionStorageQueries(); onMount(() => { if ($passphraseStore === '') { - goto('/setup/start'); + goto('/setup-pass/start'); } }); @@ -14,7 +17,14 @@ await keysStore.generate($passphraseStore); if ($keysStore.keys) { $passphraseStore = ''; - goto('/setup/download'); + + if ($keysStore.keys.device) { + $storeDeviceKey.mutate($keysStore.keys.device, { + onSuccess: () => { + goto('download'); + } + }); + } } } diff --git a/src/routes/setup/+layout.svelte b/src/routes/setup-pass/+layout.svelte similarity index 58% rename from src/routes/setup/+layout.svelte rename to src/routes/setup-pass/+layout.svelte index 725276c..2cdee35 100644 --- a/src/routes/setup/+layout.svelte +++ b/src/routes/setup-pass/+layout.svelte @@ -4,20 +4,16 @@ import { dismissWindow } from '$helpers'; import { sessionStorageQueries } from '$queries'; - const goBack = () => window.history.back(); + const goBack = () => window?.history.back(); - const restrictedPaths = ['start', 'download', 'done']; - $: allowGoBack = !restrictedPaths.some((path) => $page.url.pathname.includes(path)); - - const { setupQuery } = sessionStorageQueries(); - - $: if ($setupQuery.data && !$page.url.pathname.includes('app-password')) { + const { setupDeviceKeyQuery } = sessionStorageQueries(); + $: if ($setupDeviceKeyQuery.data) { dismissWindow(); } - {#if allowGoBack} + {#if !$page.url.pathname.includes('start')} - import { keysStore } from '$stores'; import { goto } from '$app/navigation'; import { AppParagraph, Button, Title } from '$components'; - import { onMount } from 'svelte'; import { sessionStorageQueries } from '$queries'; import { dismissWindow } from '$helpers'; import InputPassword from '$components/InputPassword.svelte'; - const { passwordWithDeviceKeyMutation } = sessionStorageQueries(); - - onMount(() => { - if ($keysStore.keys.device === null) { - goto('/setup/start'); - } - }); - - const setPassword = async (): Promise => { - if ($keysStore.keys.device !== null) { - $passwordWithDeviceKeyMutation.mutate( - { - password, - secretData: $keysStore.keys.device - }, - { - onSuccess: () => { - keysStore.resetAll(); - goto('/done'); - } - } - ); - } - }; + const { createPassword } = sessionStorageQueries(); let confirmPassword = ''; let password = ''; @@ -61,5 +36,14 @@
diff --git a/src/routes/setup/enter-passphrase/+page.svelte b/src/routes/setup-pass/enter-passphrase/+page.svelte similarity index 84% rename from src/routes/setup/enter-passphrase/+page.svelte rename to src/routes/setup-pass/enter-passphrase/+page.svelte index 8d4a0b8..75927a0 100644 --- a/src/routes/setup/enter-passphrase/+page.svelte +++ b/src/routes/setup-pass/enter-passphrase/+page.svelte @@ -4,12 +4,21 @@ import type { SetSecret } from '$lib/types'; import { goto } from '$app/navigation'; import EnterSecretComponent from './EnterSecretComponent.svelte'; + import { sessionStorageQueries } from '$queries'; let passphraseState: SetSecret = 'set'; let confirmPassphrase = ''; let showDialog = false; $: charCount = $passphraseStore.length; + + const { setupPasswordQuery } = sessionStorageQueries(); + + $: { + if ($setupPasswordQuery.data === false) { + goto('start'); + } + } {#if passphraseState === 'set'} @@ -37,6 +46,6 @@ description="Tying loose ends, please enter your passphrase again." nextLabel="Next" inputState={confirmPassphrase !== $passphraseStore ? 'Passphrases do not match' : ``} - next={() => goto('/setup/generate-keys')} + next={() => goto('/setup-keys/generate-keys')} /> {/if} diff --git a/src/routes/setup/enter-passphrase/EnterSecretComponent.svelte b/src/routes/setup-pass/enter-passphrase/EnterSecretComponent.svelte similarity index 100% rename from src/routes/setup/enter-passphrase/EnterSecretComponent.svelte rename to src/routes/setup-pass/enter-passphrase/EnterSecretComponent.svelte diff --git a/src/routes/setup/enter-passphrase/PopupDialog.svelte b/src/routes/setup-pass/enter-passphrase/PopupDialog.svelte similarity index 100% rename from src/routes/setup/enter-passphrase/PopupDialog.svelte rename to src/routes/setup-pass/enter-passphrase/PopupDialog.svelte diff --git a/src/routes/setup/start/+page.svelte b/src/routes/setup-pass/start/+page.svelte similarity index 92% rename from src/routes/setup/start/+page.svelte rename to src/routes/setup-pass/start/+page.svelte index 461a53c..3a664a0 100644 --- a/src/routes/setup/start/+page.svelte +++ b/src/routes/setup-pass/start/+page.svelte @@ -20,5 +20,5 @@ extraProps="my-4 max-w-xs" text="First time user? Select “first time setup” below. If you have already setup the key manager in the past you can select “Import existing keys" /> -