diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte index 1777bca..411b3af 100644 --- a/src/lib/components/Login.svelte +++ b/src/lib/components/Login.svelte @@ -8,7 +8,7 @@ const isValid = await passwordExistStore.validate(password); if (isValid) { - sessionStore.set({ session: true }); + sessionStore.set(true); location.reload(); } else { alert('Invalid password'); diff --git a/src/lib/const/index.ts b/src/lib/const/index.ts new file mode 100644 index 0000000..2e3e7a2 --- /dev/null +++ b/src/lib/const/index.ts @@ -0,0 +1,2 @@ +export * from './secure-store'; +export * from './query-keys'; diff --git a/src/lib/const/query-keys.ts b/src/lib/const/query-keys.ts new file mode 100644 index 0000000..1f4b669 --- /dev/null +++ b/src/lib/const/query-keys.ts @@ -0,0 +1 @@ +export const GENERATED_KEYS = 'generatedKeys'; diff --git a/src/lib/const/secure-store.ts b/src/lib/const/secure-store.ts new file mode 100644 index 0000000..1acb53f --- /dev/null +++ b/src/lib/const/secure-store.ts @@ -0,0 +1,5 @@ +export const SESSION = 'session'; +export const LOCAL = 'local'; + +export const SESSION_DATA = 'sessionData'; +export const PASSWORD_WITH_DEVICE_KEY = 'passwordWithDeviceKey'; diff --git a/src/lib/helpers/encryption.ts b/src/lib/helpers/encryption.ts index cf05a8c..ce29c63 100644 --- a/src/lib/helpers/encryption.ts +++ b/src/lib/helpers/encryption.ts @@ -1,63 +1,77 @@ import type { SecureData } from '$types'; +function 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); + } + return bytes.buffer; +} + +function arrayBufferToBase64(buffer: ArrayBuffer): string { + return 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; +} + +function arrayBufferToHexString(buffer: ArrayBuffer) { + const byteArray = new Uint8Array(buffer); + let hexString = ''; + byteArray.forEach(function (byte) { + hexString += ('0' + byte.toString(16)).slice(-2); + }); + return hexString; +} + export async function hashPassword(password: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(password); - const algo: { name: string } = { name: 'SHA-256' }; + const algo = { 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; + return arrayBufferToHexString(hashBuffer); // Convert ArrayBuffer to hex string } export async function encryptData( secretData: Uint8Array, - passwordHash: string + passwordHashHex: string // Accept hex 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 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); return { - encryptedData: new Uint8Array(encrypted), - iv + encryptedData: arrayBufferToBase64(encrypted), + iv: arrayBufferToBase64(iv) }; } export async function decryptData( encryptedData: SecureData, - passwordHash: string + passwordHashHex: string // Accept hex string ): Promise { - const algo: AesGcmParams = { name: 'AES-GCM', iv: encryptedData.iv }; - const key = await crypto.subtle.importKey( - 'raw', - new TextEncoder().encode(passwordHash), + const iv = base64ToArrayBuffer(encryptedData.iv); + const algo = { name: 'AES-GCM', iv }; + const passwordHash = hexStringToArrayBuffer(passwordHashHex); // Convert hex string to ArrayBuffer + const key = await crypto.subtle.importKey('raw', passwordHash, algo, false, ['decrypt']); + + const decrypted = await crypto.subtle.decrypt( algo, - false, - ['decrypt'] + key, + base64ToArrayBuffer(encryptedData.encryptedData) ); - - 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/queries/index.ts b/src/lib/queries/index.ts index f6fa3e5..66004a6 100644 --- a/src/lib/queries/index.ts +++ b/src/lib/queries/index.ts @@ -1,2 +1 @@ export * from './app-queries'; -export * from './query-keys'; diff --git a/src/lib/queries/query-keys.ts b/src/lib/queries/query-keys.ts deleted file mode 100644 index b66c6f7..0000000 --- a/src/lib/queries/query-keys.ts +++ /dev/null @@ -1 +0,0 @@ -export const generatedKeys = 'generatedKeys'; diff --git a/src/lib/services/storage.ts b/src/lib/services/storage.ts index baa0e94..89126f1 100644 --- a/src/lib/services/storage.ts +++ b/src/lib/services/storage.ts @@ -1,15 +1,14 @@ import { isChromeStorageSafe } from '$helpers'; -import type { AreaName, ChangesType, SecureData } from '$types'; +import type { AreaName, ChangesType, PasswordAndSecureData } from '$types'; +import type { LOCAL, SESSION, SESSION_DATA, PASSWORD_WITH_DEVICE_KEY } from '$const'; type StorageItem = - | { key: 'sessionData'; value: boolean; area: 'session' } - | { key: 'password'; value: string; area: 'local' } - | { key: 'encryptedDeviceKey'; value: SecureData; area: 'local' }; + | { key: typeof SESSION_DATA; value: boolean; area: typeof SESSION } + | { key: typeof PASSWORD_WITH_DEVICE_KEY; value: PasswordAndSecureData; area: typeof LOCAL }; type GetStorageItem = - | { key: 'sessionData'; area: 'session' } - | { key: 'password'; area: 'local' } - | { key: 'encryptedDeviceKey'; area: 'local' }; + | { key: typeof SESSION_DATA; area: typeof SESSION } + | { key: typeof PASSWORD_WITH_DEVICE_KEY; area: typeof LOCAL }; type StorageService = { set: (item: StorageItem) => void; diff --git a/src/lib/stores/const.ts b/src/lib/stores/const.ts deleted file mode 100644 index e1f607b..0000000 --- a/src/lib/stores/const.ts +++ /dev/null @@ -1 +0,0 @@ -export const passphrase = 'passphrase'; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 442303c..f666acc 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,6 +1,4 @@ export * from './passphrase'; export * from './password-exist'; -export * from './password'; -export * from './const'; export * from './keys-store'; export * from './session'; diff --git a/src/lib/stores/password-exist.ts b/src/lib/stores/password-exist.ts index 627448e..f020bf6 100644 --- a/src/lib/stores/password-exist.ts +++ b/src/lib/stores/password-exist.ts @@ -1,23 +1,28 @@ -import { z } from 'zod'; import { writable } from 'svelte/store'; import { storageService } from '$services'; -import type { AreaName, ChangesType } from '$types'; +import { PasswordAndSecureDataSchema, type AreaName, type ChangesType } from '$types'; import { hashPassword } from '$helpers'; +import { LOCAL, PASSWORD_WITH_DEVICE_KEY } from '$const'; const createPasswordExistStore = () => { - const { subscribe, set, update } = writable(null, () => { + 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' && 'password' in changes) { - update(() => true); + if (namespace === LOCAL) { + validateAndUpdate(changes[PASSWORD_WITH_DEVICE_KEY]); } }; storageService.get( { - key: 'password', - area: 'local' + key: PASSWORD_WITH_DEVICE_KEY, + area: LOCAL }, - (result) => set(!!result) + (result: unknown) => validateAndUpdate(result) ); storageService.addListener(listener); @@ -31,12 +36,14 @@ const createPasswordExistStore = () => { subscribe, validate: async (password: string) => { const result = await storageService.getWithoutCallback({ - key: 'password', - area: 'local' + key: PASSWORD_WITH_DEVICE_KEY, + area: LOCAL }); - const validatedResult = z.string().safeParse(result); + const validatedResult = PasswordAndSecureDataSchema.safeParse(result); - return validatedResult.success && (await hashPassword(password)) === validatedResult.data; + return ( + validatedResult.success && (await hashPassword(password)) === validatedResult.data.password + ); } }; }; diff --git a/src/lib/stores/password.ts b/src/lib/stores/password.ts deleted file mode 100644 index 8d71801..0000000 --- a/src/lib/stores/password.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { writable } from 'svelte/store'; - -const createPasswordStore = () => { - const { subscribe, set } = writable(''); - - return { - subscribe, - set - }; -}; - -const passwordStore = createPasswordStore(); - -export { passwordStore }; diff --git a/src/lib/stores/session.ts b/src/lib/stores/session.ts index 1a268b4..fc16d0d 100644 --- a/src/lib/stores/session.ts +++ b/src/lib/stores/session.ts @@ -1,29 +1,27 @@ 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') { - const newValue = SessionStateSchema.safeParse(changes['sessionData']); - update(() => (newValue.success ? newValue.data : false)); + if (namespace === SESSION) { + validateAndUpdate(changes[SESSION_DATA]); } }; storageService.get( { - key: 'sessionData', - area: 'session' + key: SESSION_DATA, + area: SESSION }, - (result: unknown) => { - const validatedResult = SessionStateSchema.safeParse(result); - if (validatedResult.success) { - set(validatedResult.data); - } else { - set(false); - } - } + (result: unknown) => validateAndUpdate(result) ); storageService.addListener(listener); @@ -38,9 +36,9 @@ const createSessionStore = () => { set: (value: SessionState) => { set(value); storageService.set({ - key: 'sessionData', + key: SESSION_DATA, value: value ?? false, - area: 'session' + area: SESSION }); } }; diff --git a/src/lib/types/keys.ts b/src/lib/types/keys.ts index 402952d..0cb03a0 100644 --- a/src/lib/types/keys.ts +++ b/src/lib/types/keys.ts @@ -13,9 +13,18 @@ export type KeysState = { loading: boolean; }; +export const Password = z.string(); + export const SecureDataSchema = z.object({ - encryptedData: z.instanceof(Uint8Array), - iv: z.instanceof(Uint8Array) + encryptedData: z.string(), + iv: z.string() }); export type SecureData = z.infer; + +export const PasswordAndSecureDataSchema = z.object({ + password: Password, + secureData: SecureDataSchema +}); + +export type PasswordAndSecureData = z.infer; diff --git a/src/lib/types/storage-service.ts b/src/lib/types/storage-service.ts index c1f7a63..528dc4f 100644 --- a/src/lib/types/storage-service.ts +++ b/src/lib/types/storage-service.ts @@ -1,6 +1,7 @@ +import type { LOCAL, SESSION } from '$const'; import { z } from 'zod'; -export type AreaName = 'session' | 'local' | 'sync' | 'managed'; +export type AreaName = typeof SESSION | typeof LOCAL | 'sync' | 'managed'; export const SessionStateSchema = z.boolean().nullable(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5392c2a..bcaedc5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,4 +1,4 @@ - {#if appPasswordState === 'set'} setPassword($passwordStore)} + inputState={confirmPassword !== password ? 'Passwords do not match' : ``} + next={() => setPassword(password)} /> {/if} diff --git a/src/routes/setup/done/+page.svelte b/src/routes/setup/done/+page.svelte new file mode 100644 index 0000000..da084a3 --- /dev/null +++ b/src/routes/setup/done/+page.svelte @@ -0,0 +1,16 @@ + + +Holo Key Manager Logo +Setup Complete +
+ + +
diff --git a/src/routes/setup/start/+page.svelte b/src/routes/setup/start/+page.svelte index da40c3c..461a53c 100644 --- a/src/routes/setup/start/+page.svelte +++ b/src/routes/setup/start/+page.svelte @@ -1,4 +1,4 @@ - diff --git a/static/manifest.json b/static/manifest.json index 85a9695..df7c25d 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.19", + "version": "0.0.20", "manifest_version": 3, "browser_specific_settings": { "gecko": { diff --git a/svelte.config.js b/svelte.config.js index fb9d2a9..51e90fd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,6 +12,7 @@ const config = { appDir: 'app', alias: { $components: path.resolve('./src/lib/components'), + $const: path.resolve('./src/lib/const'), $helpers: path.resolve('./src/lib/helpers'), $stores: path.resolve('./src/lib/stores'), $types: path.resolve('./src/lib/types'),