diff --git a/package.json b/package.json index 8d01f2a..3701559 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "vite build --watch", - "build": "vite build && tsc --p scripts/tsconfig.json && node removeInlineScript.cjs", + "build": "vite build && node removeInlineScript.cjs", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -42,6 +42,7 @@ "jszip": "^3.10.1", "svelte": "^4.0.5", "tailwindcss": "^3.3.3", - "tiny-glob": "^0.2.9" + "tiny-glob": "^0.2.9", + "zod": "^3.22.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 096c459..fa5351c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: tiny-glob: specifier: ^0.2.9 version: 0.2.9 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@sveltejs/adapter-static': @@ -2594,6 +2597,10 @@ packages: engines: {node: '>=10'} dev: true + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + github.com/mrruby/hcSeedBundle/1da3964a5d6af048b67594a15c2f0f43aad82b77: resolution: {tarball: https://codeload.github.com/mrruby/hcSeedBundle/tar.gz/1da3964a5d6af048b67594a15c2f0f43aad82b77} name: '@holochain/hc-seed-bundle' diff --git a/scripts/background.ts b/scripts/background.ts deleted file mode 100644 index 8599f20..0000000 --- a/scripts/background.ts +++ /dev/null @@ -1,33 +0,0 @@ -console.log('Background script loaded'); - -async function hashPassword(password: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(password); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); - return hashHex; -} - -async function storePasswordHash(password: string): Promise { - const hashedPassword = await hashPassword(password); - chrome.storage.local.set({ passwordHash: hashedPassword }); -} - -async function validatePassword(password: string): Promise { - const hashedPassword = await hashPassword(password); - const storedHash = (await chrome.storage.local.get('passwordHash')).passwordHash; - return hashedPassword === storedHash; -} - -chrome.runtime.onMessage.addListener(async (message: any, sender, sendResponse) => { - if (message.type === 'SETUP_PASSWORD') { - await storePasswordHash(message.password); - sendResponse({ status: 'success' }); - return true; // Keeps the message channel open - } - if (message.type === 'VALIDATE_PASSWORD') { - const isValid = await validatePassword(message.password); - chrome.runtime.sendMessage({ type: 'VALIDATION_RESPONSE', isValid }); - } -}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index 655e379..0000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "outDir": "../build/scripts", - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "target": "esnext", - "types": ["chrome"] - }, - "include": ["./background.ts"] -} diff --git a/src/lib/components/EnterSecretComponent.svelte b/src/lib/components/EnterSecretComponent.svelte index 842a044..8077ce4 100644 --- a/src/lib/components/EnterSecretComponent.svelte +++ b/src/lib/components/EnterSecretComponent.svelte @@ -3,7 +3,7 @@ import Button from '$components/Button.svelte'; import Tooltip from './Tooltip.svelte'; import Title from '$components/Title.svelte'; - import { dismissExtensionWindow } from '$lib/helpers'; + import { dismissWindow } from '$lib/helpers'; import clsx from 'clsx'; export let inputValue: string; @@ -43,6 +43,6 @@
-
diff --git a/src/lib/components/Init.svelte b/src/lib/components/Init.svelte new file mode 100644 index 0000000..547d1e1 --- /dev/null +++ b/src/lib/components/Init.svelte @@ -0,0 +1,29 @@ + + +
+
+ Holo Key Manager Logo + +
+ +
+ Setup +

Setup Required

+ +
+ +
diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte new file mode 100644 index 0000000..4227fa3 --- /dev/null +++ b/src/lib/components/Login.svelte @@ -0,0 +1,39 @@ + + +
+
+ Holo Key Manager Logo + +
+ +
+ Login +

Login Required

+ + +
+ +
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 81482f1..1264057 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -2,3 +2,5 @@ export { default as AppParagraph } from './AppParagraph.svelte'; export { default as Button } from './Button.svelte'; export { default as Title } from './Title.svelte'; export { default as EnterSecretComponent } from './EnterSecretComponent.svelte'; +export { default as Init } from './Init.svelte'; +export { default as Login } from './Login.svelte'; diff --git a/src/lib/helpers/auth.ts b/src/lib/helpers/auth.ts new file mode 100644 index 0000000..08c467a --- /dev/null +++ b/src/lib/helpers/auth.ts @@ -0,0 +1,8 @@ +import { passwordExistStore, sessionStore } from '$stores'; +import { derived, type Readable } from 'svelte/store'; + +export function createIsAuthenticated(): Readable { + return derived([sessionStore, passwordExistStore], ([$sessionStore, $passwordExistStore]) => + Boolean($sessionStore.session && $passwordExistStore) + ); +} diff --git a/src/lib/helpers/index.ts b/src/lib/helpers/index.ts index bf9560f..6f8f9dd 100644 --- a/src/lib/helpers/index.ts +++ b/src/lib/helpers/index.ts @@ -1,2 +1,4 @@ export * from './navigation'; export * from './other'; +export * from './password'; +export * from './auth'; diff --git a/src/lib/helpers/navigation.ts b/src/lib/helpers/navigation.ts index 8814b0b..33ba9d0 100644 --- a/src/lib/helpers/navigation.ts +++ b/src/lib/helpers/navigation.ts @@ -1,3 +1,3 @@ -export function dismissExtensionWindow() { +export function dismissWindow() { return window.close(); } diff --git a/src/lib/helpers/password.ts b/src/lib/helpers/password.ts new file mode 100644 index 0000000..d964457 --- /dev/null +++ b/src/lib/helpers/password.ts @@ -0,0 +1,8 @@ +export async function hashPassword(password: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; +} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index bfd61b7..a9614df 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -1 +1,2 @@ export * from './generate-keys'; +export * from './storage'; diff --git a/src/lib/services/storage.ts b/src/lib/services/storage.ts new file mode 100644 index 0000000..51a09e1 --- /dev/null +++ b/src/lib/services/storage.ts @@ -0,0 +1,48 @@ +import { isChromeStorageSafe } from '$helpers'; +import type { AreaName, ChangesType, StorageKey } from '$types'; + +type StorageService = { + set(key: StorageKey, value: unknown, area: 'local' | 'session'): void; + get(key: StorageKey, callback: (value: unknown) => void, area: 'local' | 'session'): void; + getWithoutCallback: (key: StorageKey, area: 'local' | 'session') => Promise; + addListener(listener: (changes: ChangesType, namespace: AreaName) => void): void; + removeListener(listener: (changes: ChangesType, namespace: AreaName) => void): void; +}; + +export const storageService: StorageService = { + set: (key: StorageKey, value: unknown, area: 'local' | 'session') => { + if (isChromeStorageSafe()) { + chrome.storage[area].set({ [key]: value }); + } + }, + get: (key: StorageKey, callback: (value: unknown) => void, area: 'local' | 'session') => { + if (isChromeStorageSafe()) { + chrome.storage[area].get([key], (result: ChangesType) => { + callback(result[key]); + }); + } else { + callback(null); + } + }, + getWithoutCallback: (key: StorageKey, area: 'local' | 'session') => { + if (isChromeStorageSafe()) { + return new Promise((resolve) => { + chrome.storage[area].get([key], (result: ChangesType) => { + resolve(result[key]); + }); + }); + } else { + return Promise.resolve(null); + } + }, + addListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => { + if (isChromeStorageSafe()) { + chrome.storage.onChanged.addListener(listener); + } + }, + removeListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => { + if (isChromeStorageSafe()) { + chrome.storage.onChanged.removeListener(listener); + } + } +}; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 956efa4..442303c 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,4 +1,5 @@ export * from './passphrase'; +export * from './password-exist'; export * from './password'; export * from './const'; export * from './keys-store'; diff --git a/src/lib/stores/password-exist.ts b/src/lib/stores/password-exist.ts new file mode 100644 index 0000000..fcb692d --- /dev/null +++ b/src/lib/stores/password-exist.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { writable } from 'svelte/store'; +import { storageService } from '$services'; +import type { AreaName, ChangesType } from '$types'; +import { hashPassword } from '$helpers'; + +const createPasswordExistStore = () => { + const { subscribe, set, update } = writable(null, () => { + const listener = (changes: ChangesType, namespace: AreaName) => { + if (namespace === 'local' && 'password' in changes) { + update(() => true); + } + }; + + storageService.get('password', (result) => set(!!result), 'local'); + + storageService.addListener(listener); + + return () => { + storageService.removeListener(listener); + }; + }); + + return { + subscribe, + set: (value: boolean) => { + set(value); + storageService.set('password', value, 'local'); + }, + validate: async (password: string) => { + const result = await storageService.getWithoutCallback('password', 'local'); + const validatedResult = z.string().safeParse(result); + + return validatedResult.success && (await hashPassword(password)) === validatedResult.data; + } + }; +}; + +const passwordExistStore = createPasswordExistStore(); + +export { passwordExistStore }; diff --git a/src/lib/stores/session.ts b/src/lib/stores/session.ts index 36afa6f..658725e 100644 --- a/src/lib/stores/session.ts +++ b/src/lib/stores/session.ts @@ -1,27 +1,33 @@ -import { isChromeStorageSafe } from '$helpers'; -import { STORAGE_KEY, type ChangesType, type SessionState } from '$types'; import { writable } from 'svelte/store'; +import { type SessionState, SessionStateSchema, type AreaName, type ChangesType } from '$types'; +import { storageService } from '$services'; const createSessionStore = () => { - const { subscribe, set, update } = writable({ session: false }, () => { - const listener = (changes: ChangesType, namespace: string) => { - if (namespace === 'session' && changes[STORAGE_KEY]?.newValue) { - console.log('Session store updated from chrome.storage.session'); - update(() => changes[STORAGE_KEY]?.newValue ?? { session: false }); + const { subscribe, set, update } = writable({ session: null }, () => { + const listener = (changes: ChangesType, namespace: AreaName) => { + if (namespace === 'session') { + const newValue = SessionStateSchema.safeParse(changes['sessionData']); + update(() => (newValue.success ? newValue.data : { session: false })); } }; - // When the store is first created, try to load existing state from chrome.storage.session - isChromeStorageSafe() && - chrome.storage.session.get([STORAGE_KEY], (result) => { - result[STORAGE_KEY] && set(result[STORAGE_KEY]); - }); + storageService.get( + 'sessionData', + (result) => { + const validatedResult = SessionStateSchema.safeParse(result); + if (validatedResult.success) { + set(validatedResult.data); + } else { + set({ session: false }); + } + }, + 'session' + ); - isChromeStorageSafe() && chrome.storage.onChanged.addListener(listener); + storageService.addListener(listener); - // Return a function that can be called to unsubscribe from the store return () => { - isChromeStorageSafe() && chrome.storage.onChanged.removeListener(listener); + storageService.removeListener(listener); }; }); @@ -29,11 +35,9 @@ const createSessionStore = () => { subscribe, set: (value: SessionState) => { set(value); - isChromeStorageSafe() && chrome.storage.session.set({ [STORAGE_KEY]: value }); + storageService.set('sessionData', value, 'session'); } }; }; -const sessionStore = createSessionStore(); - -export { sessionStore }; +export const sessionStore = createSessionStore(); diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 47b7e7a..f4824c0 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1 +1,2 @@ -export * from './password'; +export * from './keys'; +export * from './storage-service'; diff --git a/src/lib/types/password.ts b/src/lib/types/keys.ts similarity index 57% rename from src/lib/types/password.ts rename to src/lib/types/keys.ts index cd64653..d290d29 100644 --- a/src/lib/types/password.ts +++ b/src/lib/types/keys.ts @@ -10,15 +10,3 @@ export type KeysState = { keys: GeneratedKeys; loading: boolean; }; - -export type SessionState = { - session: boolean; -}; - -export const STORAGE_KEY = 'sessionData'; - -export type ChangesType = { - [STORAGE_KEY]?: { - newValue: SessionState; - }; -}; diff --git a/src/lib/types/storage-service.ts b/src/lib/types/storage-service.ts new file mode 100644 index 0000000..e98ddec --- /dev/null +++ b/src/lib/types/storage-service.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export type AreaName = 'session' | 'local' | 'sync' | 'managed'; + +export const SessionStateSchema = z.object({ + session: z.boolean().nullable() +}); + +export type SessionState = z.infer; + +export type StorageKey = 'sessionData' | 'password' | 'encryptedDeviceKey'; + +export type ChangesType = { + [key: string]: unknown; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a2071d2..52f9e0e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,33 +1,29 @@ - -
- {#if $sessionStore.session} - session - {/if} -
- Holo Key Manager Logo - -
+ let loading = true; -
- Setup -

Setup Required

- -
+ onMount(() => { + const storeValue = derived( + [sessionStore, passwordExistStore], + ([$sessionStore, $passwordExistStore]) => + $sessionStore.session === null || $passwordExistStore === null + ); + storeValue.subscribe(($loading) => { + loading = $loading; + }); + }); + -
+{#if loading} + Loading +{:else if $sessionStore.session} + Session +{:else if $passwordExistStore} + +{:else} + +{/if} diff --git a/src/routes/setup/+layout.svelte b/src/routes/setup/+layout.svelte index 9942811..5651ef1 100644 --- a/src/routes/setup/+layout.svelte +++ b/src/routes/setup/+layout.svelte @@ -1,22 +1,37 @@
{#if allowGoBack} - diff --git a/src/routes/setup/app-password/+page.svelte b/src/routes/setup/app-password/+page.svelte index fd43c0d..af20021 100644 --- a/src/routes/setup/app-password/+page.svelte +++ b/src/routes/setup/app-password/+page.svelte @@ -5,6 +5,8 @@ import { goto } from '$app/navigation'; import { EnterSecretComponent } from '$components'; import { onMount } from 'svelte'; + import { hashPassword } from '$helpers'; + import { storageService } from '$services'; onMount(() => { if ($keysStore.keys.device === null) { @@ -12,16 +14,8 @@ } }); - function sendPasswordSetupRequest(password: string): void { - chrome.runtime.sendMessage( - { - type: 'SETUP_PASSWORD', - password - }, - (response) => { - console.log('received user data', response); - } - ); + async function setPassword(password: string): Promise { + storageService.set('password', await hashPassword(password), 'local'); } let appPasswordState: SetSecret = 'set'; @@ -55,6 +49,6 @@ description="Tying loose ends, please enter your passphrase again." nextLabel="Next" inputState={confirmPassword !== $passwordStore ? 'Passwords do not match' : ``} - next={() => sendPasswordSetupRequest($passwordStore)} + next={() => setPassword($passwordStore)} /> {/if} diff --git a/src/routes/setup/generate-keys/+page.svelte b/src/routes/setup/generate-keys/+page.svelte index 4103e8d..08ef9ef 100644 --- a/src/routes/setup/generate-keys/+page.svelte +++ b/src/routes/setup/generate-keys/+page.svelte @@ -1,6 +1,6 @@