diff --git a/src/lib/helpers/auth.ts b/src/lib/helpers/auth.ts index 08c467a..ac343ae 100644 --- a/src/lib/helpers/auth.ts +++ b/src/lib/helpers/auth.ts @@ -3,6 +3,6 @@ import { derived, type Readable } from 'svelte/store'; export function createIsAuthenticated(): Readable { return derived([sessionStore, passwordExistStore], ([$sessionStore, $passwordExistStore]) => - Boolean($sessionStore.session && $passwordExistStore) + Boolean($sessionStore && $passwordExistStore) ); } diff --git a/src/lib/helpers/encryption.ts b/src/lib/helpers/encryption.ts new file mode 100644 index 0000000..cf05a8c --- /dev/null +++ b/src/lib/helpers/encryption.ts @@ -0,0 +1,63 @@ +import type { SecureData } from '$types'; + +export async function hashPassword(password: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const algo: { name: string } = { name: 'SHA-256' }; + const hashBuffer = await crypto.subtle.digest(algo, data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; +} + +export async function encryptData( + secretData: Uint8Array, + passwordHash: string +): Promise { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const algo: AesGcmParams = { name: 'AES-GCM', iv }; + const key = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(passwordHash), + algo, + false, + ['encrypt'] + ); + + const encrypted = await crypto.subtle.encrypt(algo, key, secretData); + return { + encryptedData: new Uint8Array(encrypted), + iv + }; +} + +export async function decryptData( + encryptedData: SecureData, + passwordHash: string +): Promise { + const algo: AesGcmParams = { name: 'AES-GCM', iv: encryptedData.iv }; + const key = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(passwordHash), + algo, + false, + ['decrypt'] + ); + + const decrypted = await crypto.subtle.decrypt(algo, key, encryptedData.encryptedData); + return new Uint8Array(decrypted); +} + +export function arrayBufferToBase64(buffer: ArrayBuffer): string { + return btoa(String.fromCharCode(...new Uint8Array(buffer))); +} + +export 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; +} diff --git a/src/lib/helpers/index.ts b/src/lib/helpers/index.ts index 6f8f9dd..fdbf339 100644 --- a/src/lib/helpers/index.ts +++ b/src/lib/helpers/index.ts @@ -1,4 +1,4 @@ export * from './navigation'; export * from './other'; -export * from './password'; +export * from './encryption'; export * from './auth'; diff --git a/src/lib/helpers/other.ts b/src/lib/helpers/other.ts index 25b57db..207d2c3 100644 --- a/src/lib/helpers/other.ts +++ b/src/lib/helpers/other.ts @@ -1,2 +1,4 @@ +export const isChromeDefined = () => typeof chrome !== 'undefined'; + export const isChromeStorageSafe = () => - typeof chrome !== 'undefined' && chrome.storage && chrome.storage.session; + isChromeDefined() && chrome.storage && chrome.storage.session; diff --git a/src/lib/helpers/password.ts b/src/lib/helpers/password.ts deleted file mode 100644 index d964457..0000000 --- a/src/lib/helpers/password.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/storage.ts b/src/lib/services/storage.ts index 51a09e1..baa0e94 100644 --- a/src/lib/services/storage.ts +++ b/src/lib/services/storage.ts @@ -1,21 +1,31 @@ import { isChromeStorageSafe } from '$helpers'; -import type { AreaName, ChangesType, StorageKey } from '$types'; +import type { AreaName, ChangesType, SecureData } from '$types'; + +type StorageItem = + | { key: 'sessionData'; value: boolean; area: 'session' } + | { key: 'password'; value: string; area: 'local' } + | { key: 'encryptedDeviceKey'; value: SecureData; area: 'local' }; + +type GetStorageItem = + | { key: 'sessionData'; area: 'session' } + | { key: 'password'; area: 'local' } + | { key: 'encryptedDeviceKey'; area: 'local' }; 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; + set: (item: StorageItem) => void; + get: (item: GetStorageItem, callback: (value: unknown) => void) => void; + getWithoutCallback: (item: GetStorageItem) => 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') => { + set: ({ key, value, area }) => { if (isChromeStorageSafe()) { chrome.storage[area].set({ [key]: value }); } }, - get: (key: StorageKey, callback: (value: unknown) => void, area: 'local' | 'session') => { + get: ({ key, area }, callback) => { if (isChromeStorageSafe()) { chrome.storage[area].get([key], (result: ChangesType) => { callback(result[key]); @@ -24,7 +34,7 @@ export const storageService: StorageService = { callback(null); } }, - getWithoutCallback: (key: StorageKey, area: 'local' | 'session') => { + getWithoutCallback: ({ key, area }) => { if (isChromeStorageSafe()) { return new Promise((resolve) => { chrome.storage[area].get([key], (result: ChangesType) => { diff --git a/src/lib/stores/password-exist.ts b/src/lib/stores/password-exist.ts index fcb692d..627448e 100644 --- a/src/lib/stores/password-exist.ts +++ b/src/lib/stores/password-exist.ts @@ -12,7 +12,13 @@ const createPasswordExistStore = () => { } }; - storageService.get('password', (result) => set(!!result), 'local'); + storageService.get( + { + key: 'password', + area: 'local' + }, + (result) => set(!!result) + ); storageService.addListener(listener); @@ -23,12 +29,11 @@ const createPasswordExistStore = () => { return { subscribe, - set: (value: boolean) => { - set(value); - storageService.set('password', value, 'local'); - }, validate: async (password: string) => { - const result = await storageService.getWithoutCallback('password', 'local'); + const result = await storageService.getWithoutCallback({ + key: 'password', + area: 'local' + }); const validatedResult = z.string().safeParse(result); return validatedResult.success && (await hashPassword(password)) === validatedResult.data; diff --git a/src/lib/stores/session.ts b/src/lib/stores/session.ts index 658725e..1a268b4 100644 --- a/src/lib/stores/session.ts +++ b/src/lib/stores/session.ts @@ -3,25 +3,27 @@ import { type SessionState, SessionStateSchema, type AreaName, type ChangesType import { storageService } from '$services'; const createSessionStore = () => { - const { subscribe, set, update } = writable({ session: null }, () => { + const { subscribe, set, update } = writable(null, () => { const listener = (changes: ChangesType, namespace: AreaName) => { if (namespace === 'session') { const newValue = SessionStateSchema.safeParse(changes['sessionData']); - update(() => (newValue.success ? newValue.data : { session: false })); + update(() => (newValue.success ? newValue.data : false)); } }; storageService.get( - 'sessionData', - (result) => { + { + key: 'sessionData', + area: 'session' + }, + (result: unknown) => { const validatedResult = SessionStateSchema.safeParse(result); if (validatedResult.success) { set(validatedResult.data); } else { - set({ session: false }); + set(false); } - }, - 'session' + } ); storageService.addListener(listener); @@ -35,7 +37,11 @@ const createSessionStore = () => { subscribe, set: (value: SessionState) => { set(value); - storageService.set('sessionData', value, 'session'); + storageService.set({ + key: 'sessionData', + value: value ?? false, + area: 'session' + }); } }; }; diff --git a/src/lib/types/keys.ts b/src/lib/types/keys.ts index d290d29..402952d 100644 --- a/src/lib/types/keys.ts +++ b/src/lib/types/keys.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + export type SetSecret = 'set' | 'confirm'; export type GeneratedKeys = { @@ -10,3 +12,10 @@ export type KeysState = { keys: GeneratedKeys; loading: boolean; }; + +export const SecureDataSchema = z.object({ + encryptedData: z.instanceof(Uint8Array), + iv: z.instanceof(Uint8Array) +}); + +export type SecureData = z.infer; diff --git a/src/lib/types/storage-service.ts b/src/lib/types/storage-service.ts index e98ddec..c1f7a63 100644 --- a/src/lib/types/storage-service.ts +++ b/src/lib/types/storage-service.ts @@ -2,14 +2,10 @@ import { z } from 'zod'; export type AreaName = 'session' | 'local' | 'sync' | 'managed'; -export const SessionStateSchema = z.object({ - session: z.boolean().nullable() -}); +export const SessionStateSchema = 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 52f9e0e..321dac5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,7 +10,7 @@ const storeValue = derived( [sessionStore, passwordExistStore], ([$sessionStore, $passwordExistStore]) => - $sessionStore.session === null || $passwordExistStore === null + $sessionStore === null || $passwordExistStore === null ); storeValue.subscribe(($loading) => { loading = $loading; @@ -20,7 +20,7 @@ {#if loading} Loading -{:else if $sessionStore.session} +{:else if $sessionStore} Session {:else if $passwordExistStore} diff --git a/src/routes/setup/app-password/+page.svelte b/src/routes/setup/app-password/+page.svelte index af20021..c31317c 100644 --- a/src/routes/setup/app-password/+page.svelte +++ b/src/routes/setup/app-password/+page.svelte @@ -5,7 +5,7 @@ import { goto } from '$app/navigation'; import { EnterSecretComponent } from '$components'; import { onMount } from 'svelte'; - import { hashPassword } from '$helpers'; + import { encryptData, hashPassword } from '$helpers'; import { storageService } from '$services'; onMount(() => { @@ -14,9 +14,19 @@ } }); - async function setPassword(password: string): Promise { - storageService.set('password', await hashPassword(password), 'local'); - } + const setPassword = async (password: string): Promise => { + if ($keysStore.keys.device !== null) { + const hash = await hashPassword(password); + storageService.set({ key: 'password', value: hash, area: 'local' }); + storageService.set({ + key: 'encryptedDeviceKey', + value: await encryptData($keysStore.keys.device, password), + area: 'local' + }); + + window.close(); + } + }; let appPasswordState: SetSecret = 'set'; let confirmPassword = '';