diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte index 411b3af..166de68 100644 --- a/src/lib/components/Login.svelte +++ b/src/lib/components/Login.svelte @@ -1,18 +1,15 @@ diff --git a/src/lib/const/query-keys.ts b/src/lib/const/query-keys.ts index 1f4b669..74d5a56 100644 --- a/src/lib/const/query-keys.ts +++ b/src/lib/const/query-keys.ts @@ -1 +1,2 @@ -export const GENERATED_KEYS = 'generatedKeys'; +export const SESSION_DATA_KEY = 'sessionDataKey'; +export const SETUP_KEY = 'setupKey'; diff --git a/src/lib/helpers/auth.ts b/src/lib/helpers/auth.ts deleted file mode 100644 index ac343ae..0000000 --- a/src/lib/helpers/auth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { passwordExistStore, sessionStore } from '$stores'; -import { derived, type Readable } from 'svelte/store'; - -export function createIsAuthenticated(): Readable { - return derived([sessionStore, passwordExistStore], ([$sessionStore, $passwordExistStore]) => - Boolean($sessionStore && $passwordExistStore) - ); -} diff --git a/src/lib/helpers/encryption.ts b/src/lib/helpers/encryption.ts index ce29c63..4d96b4b 100644 --- a/src/lib/helpers/encryption.ts +++ b/src/lib/helpers/encryption.ts @@ -1,55 +1,46 @@ import type { SecureData } from '$types'; -function hexStringToArrayBuffer(hexString: string) { +const hexStringToArrayBuffer = (hexString: string) => { if (hexString.length % 2 !== 0) { throw new Error('Invalid hexString length. It must be even.'); } - const bytes = new Uint8Array(hexString.length / 2); - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(hexString.slice(i * 2, i * 2 + 2), 16); + const hexBytes = hexString.match(/.{1,2}/g); + if (hexBytes === null) { + throw new Error('Invalid hexString format. It must contain only hexadecimal characters.'); } - return bytes.buffer; -} + return new Uint8Array(hexBytes.map((byte) => parseInt(byte, 16))).buffer; +}; -function arrayBufferToBase64(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))); -} +const arrayBufferToBase64 = (buffer: ArrayBuffer): string => + btoa(String.fromCharCode(...new Uint8Array(buffer))); -function base64ToArrayBuffer(base64: string): ArrayBuffer { - const binary_string = atob(base64); - const len = binary_string.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes.buffer; -} +const base64ToArrayBuffer = (base64: string): ArrayBuffer => + new Uint8Array( + atob(base64) + .split('') + .map((char) => char.charCodeAt(0)) + ).buffer; -function arrayBufferToHexString(buffer: ArrayBuffer) { - const byteArray = new Uint8Array(buffer); - let hexString = ''; - byteArray.forEach(function (byte) { - hexString += ('0' + byte.toString(16)).slice(-2); - }); - return hexString; -} +const arrayBufferToHexString = (buffer: ArrayBuffer) => + Array.prototype.map + .call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) + .join(''); -export async function hashPassword(password: string): Promise { +export const hashPassword = async (password: string): Promise => { const encoder = new TextEncoder(); const data = encoder.encode(password); const algo = { name: 'SHA-256' }; const hashBuffer = await crypto.subtle.digest(algo, data); return arrayBufferToHexString(hashBuffer); // Convert ArrayBuffer to hex string -} +}; -export async function encryptData( +export const encryptData = async ( secretData: Uint8Array, - passwordHashHex: string // Accept hex string -): Promise { + passwordHashHex: string +): Promise => { const iv = crypto.getRandomValues(new Uint8Array(12)); const algo: AesGcmParams = { name: 'AES-GCM', iv }; const passwordHash = hexStringToArrayBuffer(passwordHashHex); - console.log('Imported key length (bytes):', passwordHash.byteLength); const key = await crypto.subtle.importKey('raw', passwordHash, algo, false, ['encrypt']); const encrypted = await crypto.subtle.encrypt(algo, key, secretData); @@ -57,12 +48,12 @@ export async function encryptData( encryptedData: arrayBufferToBase64(encrypted), iv: arrayBufferToBase64(iv) }; -} +}; -export async function decryptData( +export const decryptData = async ( encryptedData: SecureData, passwordHashHex: string // Accept hex string -): Promise { +): Promise => { const iv = base64ToArrayBuffer(encryptedData.iv); const algo = { name: 'AES-GCM', iv }; const passwordHash = hexStringToArrayBuffer(passwordHashHex); // Convert hex string to ArrayBuffer @@ -74,4 +65,4 @@ export async function decryptData( base64ToArrayBuffer(encryptedData.encryptedData) ); return new Uint8Array(decrypted); -} +}; diff --git a/src/lib/helpers/index.ts b/src/lib/helpers/index.ts index fdbf339..172b335 100644 --- a/src/lib/helpers/index.ts +++ b/src/lib/helpers/index.ts @@ -1,4 +1,3 @@ export * from './navigation'; export * from './other'; export * from './encryption'; -export * from './auth'; diff --git a/src/lib/queries/app-queries.ts b/src/lib/queries/app-queries.ts index 23bc5e4..3451376 100644 --- a/src/lib/queries/app-queries.ts +++ b/src/lib/queries/app-queries.ts @@ -1,18 +1,85 @@ -// import { createMutation, useQueryClient, createQuery } from '@tanstack/svelte-query'; -// import { generateKeys } from '$services'; -// import type { GeneratedKeys } from '$types'; -// import { generatedKeys } from './query-keys'; - -export function appQueries() { - // const queryClient = useQueryClient(); - // const generateKeysMutation = createMutation({ - // mutationFn: (passphrase: string) => generateKeys(passphrase), - // onSuccess: (data) => { - // queryClient.setQueryData([generatedKeys], data); - // } - // }); - // const generatedKeysQuery = createQuery({ - // queryKey: [generatedKeys] - // }); - // return { generateKeysMutation, generatedKeysQuery }; +import { createMutation, useQueryClient, createQuery } from '@tanstack/svelte-query'; +import { storageService } from '$services'; +import { PasswordAndSecureDataSchema, SessionStateSchema } from '$types'; +import { + LOCAL, + PASSWORD_WITH_DEVICE_KEY, + SESSION, + SESSION_DATA, + SESSION_DATA_KEY, + SETUP_KEY +} from '$const'; +import { encryptData, hashPassword } from '$helpers'; + +export function sessionStorageQueries() { + const queryClient = useQueryClient(); + const sessionQuery = createQuery({ + queryKey: [SESSION_DATA_KEY], + queryFn: async () => { + const data = await storageService.getWithoutCallback({ + key: SESSION_DATA, + area: SESSION + }); + + const validatedData = SessionStateSchema.safeParse(data); + return validatedData.success ? validatedData.data : false; + } + }); + const signInMutation = createMutation({ + mutationFn: async (password: string) => { + const result = await storageService.getWithoutCallback({ + key: PASSWORD_WITH_DEVICE_KEY, + area: LOCAL + }); + + const validatedResult = PasswordAndSecureDataSchema.safeParse(result); + + if ( + validatedResult.success && + (await hashPassword(password)) === validatedResult.data.password + ) { + return storageService.set({ + key: SESSION_DATA, + value: true, + area: SESSION + }); + } + + throw new Error('Invalid password or data'); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [SESSION_DATA_KEY] }); + } + }); + const setupQuery = createQuery({ + queryKey: [SETUP_KEY], + queryFn: async () => { + const data = await storageService.getWithoutCallback({ + key: PASSWORD_WITH_DEVICE_KEY, + area: LOCAL + }); + + const validatedData = PasswordAndSecureDataSchema.safeParse(data); + return validatedData.success; + } + }); + const passwordWithDeviceKeyMutation = createMutation({ + mutationFn: async (mutationData: { password: string; secretData: Uint8Array }) => { + const hash = await hashPassword(mutationData.password); + storageService.set({ + key: PASSWORD_WITH_DEVICE_KEY, + value: { + password: hash, + secureData: await encryptData(mutationData.secretData, hash) + }, + area: LOCAL + }); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [SETUP_KEY] }); + } + }); + + return { setupQuery, passwordWithDeviceKeyMutation, sessionQuery, signInMutation }; } diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index f666acc..af2853b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,4 +1,2 @@ export * from './passphrase'; -export * from './password-exist'; export * from './keys-store'; -export * from './session'; diff --git a/src/lib/stores/keys-store.ts b/src/lib/stores/keys-store.ts index 90e547b..2d42244 100644 --- a/src/lib/stores/keys-store.ts +++ b/src/lib/stores/keys-store.ts @@ -35,7 +35,8 @@ const initKeysStore = () => { master: null, revocation: null } - })) + })), + resetAll: () => update(() => initialState) }; }; diff --git a/src/lib/stores/password-exist.ts b/src/lib/stores/password-exist.ts deleted file mode 100644 index f020bf6..0000000 --- a/src/lib/stores/password-exist.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { writable } from 'svelte/store'; -import { storageService } from '$services'; -import { PasswordAndSecureDataSchema, type AreaName, type ChangesType } from '$types'; -import { hashPassword } from '$helpers'; -import { LOCAL, PASSWORD_WITH_DEVICE_KEY } from '$const'; - -const createPasswordExistStore = () => { - const { subscribe, update } = writable(null, () => { - const validateAndUpdate = (result: unknown) => { - const validatedResult = PasswordAndSecureDataSchema.safeParse(result); - update(() => validatedResult.success); - }; - - const listener = (changes: ChangesType, namespace: AreaName) => { - if (namespace === LOCAL) { - validateAndUpdate(changes[PASSWORD_WITH_DEVICE_KEY]); - } - }; - - storageService.get( - { - key: PASSWORD_WITH_DEVICE_KEY, - area: LOCAL - }, - (result: unknown) => validateAndUpdate(result) - ); - - storageService.addListener(listener); - - return () => { - storageService.removeListener(listener); - }; - }); - - return { - subscribe, - validate: async (password: string) => { - const result = await storageService.getWithoutCallback({ - key: PASSWORD_WITH_DEVICE_KEY, - area: LOCAL - }); - const validatedResult = PasswordAndSecureDataSchema.safeParse(result); - - return ( - validatedResult.success && (await hashPassword(password)) === validatedResult.data.password - ); - } - }; -}; - -const passwordExistStore = createPasswordExistStore(); - -export { passwordExistStore }; diff --git a/src/lib/stores/session.ts b/src/lib/stores/session.ts deleted file mode 100644 index fc16d0d..0000000 --- a/src/lib/stores/session.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { writable } from 'svelte/store'; -import { type SessionState, SessionStateSchema, type AreaName, type ChangesType } from '$types'; -import { storageService } from '$services'; -import { SESSION, SESSION_DATA } from '$const'; - -const createSessionStore = () => { - const { subscribe, set, update } = writable(null, () => { - const validateAndUpdate = (result: unknown) => { - const validatedResult = SessionStateSchema.safeParse(result); - update(() => (validatedResult.success ? validatedResult.data : false)); - }; - - const listener = (changes: ChangesType, namespace: AreaName) => { - if (namespace === SESSION) { - validateAndUpdate(changes[SESSION_DATA]); - } - }; - - storageService.get( - { - key: SESSION_DATA, - area: SESSION - }, - (result: unknown) => validateAndUpdate(result) - ); - - storageService.addListener(listener); - - return () => { - storageService.removeListener(listener); - }; - }); - - return { - subscribe, - set: (value: SessionState) => { - set(value); - storageService.set({ - key: SESSION_DATA, - value: value ?? false, - area: SESSION - }); - } - }; -}; - -export const sessionStore = createSessionStore(); diff --git a/src/lib/types/storage-service.ts b/src/lib/types/storage-service.ts index 528dc4f..8e09302 100644 --- a/src/lib/types/storage-service.ts +++ b/src/lib/types/storage-service.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export type AreaName = typeof SESSION | typeof LOCAL | 'sync' | 'managed'; -export const SessionStateSchema = z.boolean().nullable(); +export const SessionStateSchema = z.boolean(); export type SessionState = z.infer; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d5e536d..11a910e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,47 +1,15 @@ -{#if loading} +{#if $sessionQuery.isFetching || $setupQuery.isFetching} Loading -{:else if $sessionStore} +{:else if $sessionQuery.data} Session -{:else if $passwordExistStore} +{:else if $setupQuery.data} {:else} diff --git a/src/routes/setup/done/+page.svelte b/src/routes/done/+page.svelte similarity index 100% rename from src/routes/setup/done/+page.svelte rename to src/routes/done/+page.svelte diff --git a/src/routes/setup/+layout.svelte b/src/routes/setup/+layout.svelte index 071f72a..955747f 100644 --- a/src/routes/setup/+layout.svelte +++ b/src/routes/setup/+layout.svelte @@ -1,27 +1,18 @@
{ if ($keysStore.keys.device === null) { @@ -16,16 +16,18 @@ const setPassword = async (password: string): Promise => { if ($keysStore.keys.device !== null) { - const hash = await hashPassword(password); - storageService.set({ - key: PASSWORD_WITH_DEVICE_KEY, - value: { - password: hash, - secureData: await encryptData($keysStore.keys.device, hash) + $passwordWithDeviceKeyMutation.mutate( + { + password, + secretData: $keysStore.keys.device }, - area: LOCAL - }); - goto('/setup/done'); + { + onSuccess: () => { + keysStore.resetAll(); + goto('/done'); + } + } + ); } }; diff --git a/static/manifest.json b/static/manifest.json index df7c25d..27b41b2 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -1,7 +1,7 @@ { "name": "Holo key manager", "description": "A browser extension to manage holo keys", - "version": "0.0.20", + "version": "0.0.21", "manifest_version": 3, "browser_specific_settings": { "gecko": {