diff --git a/bun.lockb b/bun.lockb index 9851d42..ce79e9c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index eb20ce4..4d63e16 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@dnd-kit/abstract": "^0.2.4", "@dnd-kit/helpers": "^0.2.4", "@tanstack/match-sorter-utils": "^8.19.4", + "@types/semver": "^7.7.1", "@xterm/xterm": "^5.5.0", "@yume-chan/adb": "^2.6.0", "@yume-chan/adb-credential-web": "^2.1.0", @@ -77,6 +78,7 @@ "firebase": "^12.14.0", "idb": "^8.0.3", "mode-watcher": "^1.1.0", + "semver": "^7.8.4", "usb": "^2.17.0", "uuid": "^13.0.0", "xterm-addon-fit": "^0.8.0", diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index 455dcfd..0c817cd 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -183,9 +183,9 @@ } } - function saveSheetPrice() { + async function saveSheetPrice() { if (!canEditSheetPrice || sheetPriceValue === null) return; - sendCommandRequest('sheet', { + await sendCommandRequest('sheet', { country: get(departmentStore), content: [ { diff --git a/src/lib/components/recipe-details/value_event.ts b/src/lib/components/recipe-details/value_event.ts index 81653d5..61a9231 100644 --- a/src/lib/components/recipe-details/value_event.ts +++ b/src/lib/components/recipe-details/value_event.ts @@ -11,7 +11,7 @@ enum ValueEvent { SAVED } -function actionReport(action_name: string, values: any, currentRef: string) { +async function actionReport(action_name: string, values: any, currentRef: string) { let country = get(departmentStore) ?? 'unknown dep'; if (currentRef === 'brew') { @@ -27,7 +27,7 @@ function actionReport(action_name: string, values: any, currentRef: string) { } } - sendMessage({ + await sendMessage({ type: 'log_report', payload: { user: get(auth)?.email ?? 'unknown', diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index e593d4b..3fac5c8 100644 --- a/src/lib/components/recipe-editor-dialog.svelte +++ b/src/lib/components/recipe-editor-dialog.svelte @@ -185,7 +185,7 @@ let formatted = formatCustomDate(date); ready_to_send_brew[0].LastChange = formatted; - sendMessage({ + await sendMessage({ type: 'save_recipe', payload: { user_info, @@ -194,7 +194,7 @@ } }); } else if (get(referenceFromPage) == 'overview') { - sendMessage({ + await sendMessage({ type: 'save_recipe', payload: { user_info, diff --git a/src/lib/core/client/server.ts b/src/lib/core/client/server.ts index 98e2ea9..979c021 100644 --- a/src/lib/core/client/server.ts +++ b/src/lib/core/client/server.ts @@ -37,7 +37,7 @@ export async function getRecipes() { recipeData.set([]); recipeOverviewData.set([]); - sendMessage({ + await sendMessage({ type: 'recipe', payload: { auth: idToken ?? '', @@ -82,7 +82,7 @@ export async function getRecipeWithVersion(version: string) { // NOTE: although version is provided, actual version field is still need to be latest // Just in case version is not found - sendMessage({ + await sendMessage({ type: 'recipe', payload: { auth: idToken ?? '', diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index b875029..d1d94cf 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -37,15 +37,23 @@ import { buildOverviewFromServer } from '$lib/data/recipeService'; import { auth } from '../client/firebase'; import { type RecipeVersion } from '$lib/models/recipe_version.model'; import { goto } from '$app/navigation'; -import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore'; +import { + sharedKey as sharedKey, + socketAlreadySendHeartbeat, + socketConnectionOfflineCount +} from '../stores/websocketStore'; import type { RecipePrice } from '$lib/models/price.model'; import { sendCommandRequest, sendMessage } from './ws_messageSender'; import { auth as authStore } from '../stores/auth'; import { v4 as uuidv4 } from 'uuid'; import { handleSheetResponseFromNoti } from './sheetNotiHandler'; +import { env } from '$env/dynamic/public'; +import * as semver from 'semver'; +import { WebCryptoHelper } from '../utils/crypto'; export const messages = writable([]); +type HandshakeAck = { server_public_key: string; status: string }; type WSMessage = { type: string; payload: any }; // MAXIMUM LIMIT = 1814355 @@ -131,7 +139,7 @@ const handlers: Record void> = { } } }, - stream_data_end: (p) => { + stream_data_end: async (p) => { recipeLoading.set(false); // build overview for recipe from server @@ -154,7 +162,7 @@ const handlers: Record void> = { } // send next chain message - sendMessage({ + await sendMessage({ type: 'price', payload: { action: { @@ -352,7 +360,7 @@ const handlers: Record void> = { currentRecipeVersionsSelector.set(result); } }, - price: (p) => { + price: async (p) => { let req_action = p.req_action; let status = p.status; let to = p.to; @@ -385,7 +393,7 @@ const handlers: Record void> = { current_streaming_instance[request_id] = ''; streamingRawData.set(current_streaming_instance); - sendCommandRequest('sheet', { + await sendCommandRequest('sheet', { country: current_meta?.country ?? '', content: saved_product_code_to_get_from_sheet, param: 'price', @@ -395,59 +403,59 @@ const handlers: Record void> = { lastRequestSheetPrice.set(lastRequestPriceInstance); }, - raw_stream: (p) => { - let streamRawInstance = get(streamingRawData); - let sub_type = p.sub_type; - let request_id = p.request_id; - let size_per_chunk = p.size_per_chunk; - let total_chunks = p.total_chunks; - let idx = p.idx; + // raw_stream: (p) => { + // let streamRawInstance = get(streamingRawData); + // let sub_type = p.sub_type; + // let request_id = p.request_id; + // let size_per_chunk = p.size_per_chunk; + // let total_chunks = p.total_chunks; + // let idx = p.idx; - switch (sub_type) { - case 'price': - streamingRawMeta.set({ - id: request_id, - total_size: total_chunks, - chunk_size: size_per_chunk, - progress: 0 - }); - break; - case 'chunk_price': - streamingRawMeta.set({ - id: request_id, - total_size: total_chunks, - chunk_size: size_per_chunk, - progress: idx - }); + // switch (sub_type) { + // case 'price': + // streamingRawMeta.set({ + // id: request_id, + // total_size: total_chunks, + // chunk_size: size_per_chunk, + // progress: 0 + // }); + // break; + // case 'chunk_price': + // streamingRawMeta.set({ + // id: request_id, + // total_size: total_chunks, + // chunk_size: size_per_chunk, + // progress: idx + // }); - let raw_payload = p.raw ?? ''; - streamRawInstance[request_id] += raw_payload; - streamingRawData.set(streamRawInstance); + // let raw_payload = p.raw ?? ''; + // streamRawInstance[request_id] += raw_payload; + // streamingRawData.set(streamRawInstance); - break; - case 'end_price': - let lastRequestPriceInstance = get(lastRequestSheetPrice); - let country = lastRequestPriceInstance[request_id]; + // break; + // case 'end_price': + // let lastRequestPriceInstance = get(lastRequestSheetPrice); + // let country = lastRequestPriceInstance[request_id]; - try { - let raw_payload = JSON.parse(streamRawInstance[request_id]); - let ref_from_raw = raw_payload.payload.ref ?? ''; - let from_service_raw = raw_payload.payload.from ?? ''; - let parsed_payload = raw_payload.payload ?? ''; + // try { + // let raw_payload = JSON.parse(streamRawInstance[request_id]); + // let ref_from_raw = raw_payload.payload.ref ?? ''; + // let from_service_raw = raw_payload.payload.from ?? ''; + // let parsed_payload = raw_payload.payload ?? ''; - if (from_service_raw == 'sheet-service') { - handleSheetResponseFromNoti(parsed_payload, ref_from_raw, country); - delete streamRawInstance[request_id]; - streamingRawData.set(streamRawInstance); - } - } catch (e) { - console.log(`end price process error: ${e}`); - } + // if (from_service_raw == 'sheet-service') { + // handleSheetResponseFromNoti(parsed_payload, ref_from_raw, country); + // delete streamRawInstance[request_id]; + // streamingRawData.set(streamRawInstance); + // } + // } catch (e) { + // console.log(`end price process error: ${e}`); + // } - break; - default: - } - }, + // break; + // default: + // } + // }, heartbeat: (p) => { socketConnectionOfflineCount.set(0); socketAlreadySendHeartbeat.set(0); @@ -476,22 +484,50 @@ const handlers: Record void> = { } }; -export function handleIncomingMessages(raw: string) { - const msg: WSMessage = JSON.parse(raw); +export async function handleIncomingMessages(raw: string, clientPrivateKey: CryptoKey) { + const APP_VERSION = env.PUBLIC_APP_SEMVER; + + const ack: HandshakeAck = JSON.parse(raw); // console.log(`[WS MSG] type=${msg.type}`, msg.payload); - if (msg == null) { - // error response - addNotification('ERR:No response from server'); + if (ack != null && ack.status === 'authenticated') { + // has server response + + sharedKey.set(await WebCryptoHelper.deriveSharedKey(clientPrivateKey, ack.server_public_key)); + + addNotification('INFO:Secured Connection'); + return; } + if (semver.satisfies(APP_VERSION, '^0.0.2')) { + // secured message decryption + let sharedKeyStore = get(sharedKey); + if (sharedKeyStore) { + let raw_payload = JSON.parse(raw); + let decrypted_string = await WebCryptoHelper.decryptMessage( + sharedKeyStore, + raw_payload.ciphertext, + raw_payload.iv + ); + let actual_message: WSMessage = JSON.parse(decrypted_string); - // raw streaming type - if (msg.type.startsWith('raw_stream')) { - // convert - let sub_type = msg.type.replace('raw_stream_', ''); - msg.payload.sub_type = sub_type; - msg.type = 'raw_stream'; + handlers[actual_message.type]?.(actual_message.payload); + } + } else { + const msg: WSMessage = JSON.parse(raw); + if (msg == null) { + // error response + addNotification('ERR:No response from server'); + return; + } + + // raw streaming type + // if (msg.type.startsWith('raw_stream')) { + // // convert + // let sub_type = msg.type.replace('raw_stream_', ''); + // msg.payload.sub_type = sub_type; + // msg.type = 'raw_stream'; + // } + + handlers[msg.type]?.(msg.payload); } - - handlers[msg.type]?.(msg.payload); } diff --git a/src/lib/core/handlers/ws_messageSender.ts b/src/lib/core/handlers/ws_messageSender.ts index 5d3dd01..8719e34 100644 --- a/src/lib/core/handlers/ws_messageSender.ts +++ b/src/lib/core/handlers/ws_messageSender.ts @@ -1,8 +1,11 @@ import { get, writable } from 'svelte/store'; import type { OutMessage } from '../types/outMessage'; -import { socketStore } from '../stores/websocketStore'; +import { sharedKey, socketStore } from '../stores/websocketStore'; import { addNotification } from '../stores/noti'; import { auth } from '../stores/auth'; +import { WebCryptoHelper } from '../utils/crypto'; +import * as semver from 'semver'; +import { env } from '$env/dynamic/public'; export const queue = writable([]); @@ -18,7 +21,7 @@ function getServiceName(cmdReq: CommandRequest) { } // Websocket message wrapper for commands like `sheet`, `command` -export function sendCommandRequest(target: CommandRequest, values: any): boolean { +export async function sendCommandRequest(target: CommandRequest, values: any): Promise { let srv_name = getServiceName(target); let curr_user = get(auth); @@ -31,7 +34,7 @@ export function sendCommandRequest(target: CommandRequest, values: any): boolean }; } - return sendMessage({ + return await sendMessage({ type: target, payload: { user_info: user_info ?? {}, @@ -41,9 +44,13 @@ export function sendCommandRequest(target: CommandRequest, values: any): boolean }); } -export function sendMessage(msg: OutMessage, ignore_queue_request: boolean = true): boolean { +export async function sendMessage( + msg: OutMessage, + ignore_queue_request: boolean = true +): Promise { + const APP_VERSION = env.PUBLIC_APP_SEMVER; const socket = get(socketStore); - const data = JSON.stringify(msg); + let data = JSON.stringify(msg); // console.log('try sending ', data); @@ -64,6 +71,17 @@ export function sendMessage(msg: OutMessage, ignore_queue_request: boolean = tru return false; } + // console.log('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2')); + + if (semver.satisfies(APP_VERSION, '^0.0.2')) { + console.log('sending secured'); + let sharedKeyRes = get(sharedKey); + + // do encrypt + if (sharedKeyRes != null) + data = JSON.stringify(await WebCryptoHelper.encryptMessage(sharedKeyRes, data)); + } + socket.send(data); return true; } diff --git a/src/lib/core/services/sheetService.ts b/src/lib/core/services/sheetService.ts index 2e15f35..bb13af6 100644 --- a/src/lib/core/services/sheetService.ts +++ b/src/lib/core/services/sheetService.ts @@ -11,47 +11,51 @@ import { } from '../stores/sheetStore'; import { setGenLayoutGenerating } from '../stores/genLayoutStore'; -export function requestCatalogs(country: string): boolean { - return sendCommandRequest('sheet', { +export async function requestCatalogs(country: string): Promise { + return await sendCommandRequest('sheet', { country: country, param: 'catalogs' }); } -export function enterRoom(country: string, catalog: string): boolean { - return sendCommandRequest('sheet', { +export async function enterRoom(country: string, catalog: string): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, param: 'enter' }); } -export function sendHeartbeat(country: string, catalog: string): boolean { - return sendCommandRequest('sheet', { +export async function sendHeartbeat(country: string, catalog: string): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, param: 'heartbeat' }); } -export function exitRoom(country: string, catalog: string): boolean { - return sendCommandRequest('sheet', { +export async function exitRoom(country: string, catalog: string): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, param: 'exit' }); } -export function requestCatalogMenu(country: string, catalog: string): boolean { - return sendCommandRequest('sheet', { +export async function requestCatalogMenu(country: string, catalog: string): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, param: 'catalog/menu' }); } -export function updateMenu(country: string, catalog: string, content: any[]): boolean { - return sendCommandRequest('sheet', { +export async function updateMenu( + country: string, + catalog: string, + content: any[] +): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, content: content, @@ -59,9 +63,9 @@ export function updateMenu(country: string, catalog: string, content: any[]): bo }); } -export function addMenu(country: string, catalog: string, content: any[]): boolean { +export async function addMenu(country: string, catalog: string, content: any[]): Promise { console.log('[sheetService] Adding menu:', { country, catalog, content }); - const sent = sendCommandRequest('sheet', { + const sent = await sendCommandRequest('sheet', { country: country, catalog: catalog, content: content, @@ -71,9 +75,13 @@ export function addMenu(country: string, catalog: string, content: any[]): boole return sent; } -export function deleteMenu(country: string, catalog: string, targetIds: number[]): boolean { +export async function deleteMenu( + country: string, + catalog: string, + targetIds: number[] +): Promise { const content = targetIds.map((id) => ({ target_id: id })); - return sendCommandRequest('sheet', { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, content: content, @@ -81,12 +89,12 @@ export function deleteMenu(country: string, catalog: string, targetIds: number[] }); } -export function swapMenu( +export async function swapMenu( country: string, catalog: string, swaps: { source_id: number; target_id: number }[] -): boolean { - return sendCommandRequest('sheet', { +): Promise { + return await sendCommandRequest('sheet', { country: country, catalog: catalog, content: swaps, @@ -94,7 +102,7 @@ export function swapMenu( }); } -export function requestListMenu(country: string, boxid?: string): boolean { +export async function requestListMenu(country: string, boxid?: string): Promise { const curr_user = get(auth); let user_info: any = {}; @@ -111,7 +119,7 @@ export function requestListMenu(country: string, boxid?: string): boolean { console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid); - return sendMessage({ + return await sendMessage({ type: 'list_menu', payload: { user_info, @@ -121,7 +129,7 @@ export function requestListMenu(country: string, boxid?: string): boolean { }); } -export function requestGenLayout(country: string): boolean { +export async function requestGenLayout(country: string): Promise { const curr_user = get(auth); let user_info: any = {}; @@ -137,7 +145,7 @@ export function requestGenLayout(country: string): boolean { console.log('[sheetService] Sending gen-layout request for country:', country); - return sendMessage({ + return await sendMessage({ type: 'command', payload: { user_info, @@ -156,7 +164,7 @@ export function requestGenLayout(country: string): boolean { * Request price data from sheet for specific product codes * NOTE: Can only send once per type (price). Use hasSheetPriceBeenSent to check. */ -export function requestSheetPrice(country: string, productCodes: string[]): boolean { +export async function requestSheetPrice(country: string, productCodes: string[]): Promise { // Check if already sent if (hasSheetPriceBeenSent('price')) { console.warn('[sheetService] Price request already sent, skipping'); @@ -187,9 +195,16 @@ export function requestSheetPrice(country: string, productCodes: string[]): bool // Convert to array of objects (backend expects objects, not strings) const content = productCodes.map((code) => ({ product_code: code })); - console.log('[sheetService] Sending sheet price request for country:', country, 'codes:', productCodes.length, 'request_id:', request_id); + console.log( + '[sheetService] Sending sheet price request for country:', + country, + 'codes:', + productCodes.length, + 'request_id:', + request_id + ); - const sent = sendCommandRequest('sheet', { + const sent = await sendCommandRequest('sheet', { country: country, content: content, param: 'price', @@ -210,18 +225,23 @@ export function requestSheetPrice(country: string, productCodes: string[]): bool * Update price data in sheet * content: [{ row_index: number, cells: [{ value: string, coord: { row: number, col: number } }] }] */ -export function updateSheetPrice( +export async function updateSheetPrice( country: string, content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[] -): boolean { +): Promise { if (!content || content.length === 0) { console.warn('[sheetService] No content to update'); return false; } - console.log('[sheetService] Updating sheet price for country:', country, 'items:', content.length); + console.log( + '[sheetService] Updating sheet price for country:', + country, + 'items:', + content.length + ); - return sendCommandRequest('sheet', { + return await sendCommandRequest('sheet', { country: country, content: content, param: 'update/price' @@ -232,18 +252,24 @@ export function updateSheetPrice( * Add new price rows to sheet (for product codes that don't exist in price sheet) * content: [{ cells: [product_code, name_en, name_th, ..., price, ...] }] */ -export function addSheetPrice( +export async function addSheetPrice( country: string, content: { cells: string[] }[] -): boolean { +): Promise { if (!content || content.length === 0) { console.warn('[sheetService] No content to add'); return false; } - console.log('[sheetService] Adding price rows for country:', country, 'items:', content.length, content); + console.log( + '[sheetService] Adding price rows for country:', + country, + 'items:', + content.length, + content + ); - return sendCommandRequest('sheet', { + return await sendCommandRequest('sheet', { country: country, content: content, param: 'add/price' diff --git a/src/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts index 720fb7e..23de68c 100644 --- a/src/lib/core/stores/websocketStore.ts +++ b/src/lib/core/stores/websocketStore.ts @@ -7,16 +7,20 @@ import { auth } from '../client/firebase'; import { auth as authStore } from '$lib/core/stores/auth'; import { addNotification } from './noti'; import { permission } from './permissions'; +import { WebCryptoHelper } from '../utils/crypto'; let socket: WebSocket | null = null; let reconnectTimeout: any; let socketCheck: any; +let sendAuthInfoInterval: any; const ENABLE_WS_DEBUG: boolean = false; export const socketConnectionOfflineCount = writable(0); export const socketAlreadySendHeartbeat = writable(0); export const socketStore = writable(null); +export const sharedKey = writable(null); + export function waitForOpenSocket(timeoutMs = 8000): Promise { const currentSocket = get(socketStore); if (currentSocket?.readyState === WebSocket.OPEN) { @@ -49,7 +53,7 @@ export function waitForOpenSocket(timeoutMs = 8000): Promise { }); } -export function connectToWebsocket(id_token?: string) { +export async function connectToWebsocket(id_token?: string) { if (browser) { // console.log('connecting to ', env.PUBLIC_WSS); try { @@ -57,12 +61,12 @@ export function connectToWebsocket(id_token?: string) { return; } - let productionMode = env.PUBLIC_WSS.startsWith('wss'); - - let ws_url = productionMode ? `${env.PUBLIC_WSS}?token=${id_token}` : `${env.PUBLIC_WSS}`; + let ws_url = env.PUBLIC_WSS; socket = new WebSocket(ws_url); + sharedKey.set(null); + const { privateKey, publicKeyBase64 } = await WebCryptoHelper.generateKeyPair(); - socket.addEventListener('open', () => { + socket.addEventListener('open', async () => { socketStore.set(socket); addNotification('INFO:Connected!'); @@ -74,29 +78,40 @@ export function connectToWebsocket(id_token?: string) { let auth_data = get(authStore); let perms = get(permission); - // Debug: check if auth_data has uid - console.log('[WS Auth] Sending auth with:', { - uid: auth_data?.uid, - name: auth_data?.displayName, - email: auth_data?.email - }); + socket.send( + JSON.stringify({ + token: id_token ?? '', + client_public_key: publicKeyBase64 + }) + ); - sendMessage({ - type: 'auth', - payload: { - user: { - uid: auth_data?.uid ?? '', - name: auth_data?.displayName ?? '', - email: auth_data?.email ?? '', - permissions: perms.join(',') - } + sendAuthInfoInterval = setInterval(async () => { + if (get(sharedKey)) { + // Debug: check if auth_data has uid + console.log('[WS Auth] Sending auth info with:', { + uid: auth_data?.uid, + name: auth_data?.displayName, + email: auth_data?.email + }); + await sendMessage({ + type: 'auth', + payload: { + user: { + uid: auth_data?.uid ?? '', + name: auth_data?.displayName ?? '', + email: auth_data?.email ?? '', + permissions: perms.join(',') + } + } + }); + clearInterval(sendAuthInfoInterval); } - }); + }, 3000); } console.log(socket); // heartbeat 10s - socketCheck = setInterval(() => { + socketCheck = setInterval(async () => { if (get(socketAlreadySendHeartbeat) > 0) { let heartbeat_may_offline_count = get(socketConnectionOfflineCount); @@ -108,13 +123,13 @@ export function connectToWebsocket(id_token?: string) { socketConnectionOfflineCount.set(0); socketAlreadySendHeartbeat.set(0); - connectToWebsocket(id_token); + await connectToWebsocket(id_token); return; } if (socket != null) { - sendMessage({ + await sendMessage({ type: 'heartbeat', payload: {} }); @@ -130,18 +145,19 @@ export function connectToWebsocket(id_token?: string) { if (auth.currentUser && socket == null) { console.log('try reconnect websocket ...'); // retry again - reconnectTimeout = setTimeout(() => { - connectToWebsocket(id_token); + reconnectTimeout = setTimeout(async () => { + await connectToWebsocket(id_token); }, 5000); } }); - socket.addEventListener('message', (event) => { - handleIncomingMessages(event.data); + socket.addEventListener('message', async (event) => { + await handleIncomingMessages(event.data, privateKey); }); socket.addEventListener('close', () => { socketStore.set(null); + sharedKey.set(null); socket = null; clearInterval(socketCheck); @@ -149,13 +165,14 @@ export function connectToWebsocket(id_token?: string) { if (auth.currentUser && !socket) { console.log('try reconnect websocket ...'); // retry again - reconnectTimeout = setTimeout(() => connectToWebsocket(id_token), 5000); + reconnectTimeout = setTimeout(async () => await connectToWebsocket(id_token), 5000); } }); socket.addEventListener('error', (e) => { // console.log('WebSocket error: ', e); socketStore.set(null); + sharedKey.set(null); }); } catch (socket_error: any) { if (ENABLE_WS_DEBUG) { diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts index f51bd46..5795c9f 100644 --- a/src/lib/core/types/outMessage.ts +++ b/src/lib/core/types/outMessage.ts @@ -1,4 +1,5 @@ export type OutMessage = + | { token: any; client_public_key: any } | { type: 'chat'; payload: string } | { type: 'ping' } | { type: 'lock'; payload: { field: string } } @@ -54,7 +55,7 @@ export type OutMessage = values: any; }; } - | { + | { type: 'list_menu'; payload: { user_info: any; @@ -62,7 +63,6 @@ export type OutMessage = boxid?: string; }; } - | { type: 'price'; payload: { diff --git a/src/lib/core/utils/crypto.ts b/src/lib/core/utils/crypto.ts new file mode 100644 index 0000000..0803966 --- /dev/null +++ b/src/lib/core/utils/crypto.ts @@ -0,0 +1,68 @@ +export class WebCryptoHelper { + static async generateKeyPair() { + const keyPair = await window.crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256' + }, + true, + ['deriveKey', 'deriveBits'] + ); + + const exportedPublic = await window.crypto.subtle.exportKey('raw', keyPair.publicKey); + const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedPublic))); + + return { privateKey: keyPair.privateKey, publicKeyBase64 }; + } + + static async deriveSharedKey(clientPrivateKey: any, serverPublicKeyBase64: any) { + const binarySign = atob(serverPublicKeyBase64); + const bytes = new Uint8Array(binarySign.length); + for (let i = 0; i < binarySign.length; i++) { + bytes[i] = binarySign.charCodeAt(i); + } + + const importedServerPublic = await window.crypto.subtle.importKey( + 'raw', + bytes, + { name: 'ECDH', namedCurve: 'P-256' }, + true, + [] + ); + return await window.crypto.subtle.deriveKey( + { name: 'ECDH', public: importedServerPublic }, + clientPrivateKey, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); + } + + static async decryptMessage(aesKey: any, ciphertextBase64: any, ivBase64: any) { + const rawCipher = Uint8Array.from(atob(ciphertextBase64), (c) => c.charCodeAt(0)); + const rawIv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0)); + const decryptedBuffer = await window.crypto.subtle.decrypt( + { name: 'AES-GCM', iv: rawIv }, + aesKey, + rawCipher + ); + return new TextDecoder().decode(decryptedBuffer); + } + + // Encrypt outgoing messages before sending them to your Axum backend + static async encryptMessage(aesKey: any, plainText: any) { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12-byte nonce + const encodedText = new TextEncoder().encode(plainText); + + const ciphertextBuffer = await window.crypto.subtle.encrypt( + { name: 'AES-GCM', iv: iv }, + aesKey, + encodedText + ); + + const ciphertextBase64 = btoa(String.fromCharCode(...new Uint8Array(ciphertextBuffer))); + const ivBase64 = btoa(String.fromCharCode(...iv)); + + return { ciphertext: ciphertextBase64, iv: ivBase64 }; + } +} diff --git a/src/routes/(authed)/+layout.svelte b/src/routes/(authed)/+layout.svelte index 926c2d3..910c93e 100644 --- a/src/routes/(authed)/+layout.svelte +++ b/src/routes/(authed)/+layout.svelte @@ -97,8 +97,8 @@ websocketConnectedForUid = currentUser.uid; console.log('connect ws after auth ready'); - void currentUser.getIdToken().then((idToken) => { - connectToWebsocket(idToken); + void currentUser.getIdToken(true).then(async (idToken) => { + await connectToWebsocket(idToken); }); } diff --git a/src/routes/(authed)/recipe/overview/+page.svelte b/src/routes/(authed)/recipe/overview/+page.svelte index 8c9cf29..edd1459 100644 --- a/src/routes/(authed)/recipe/overview/+page.svelte +++ b/src/routes/(authed)/recipe/overview/+page.svelte @@ -58,9 +58,9 @@ } }); - function sendGetRecipeVersions(country: string) { + async function sendGetRecipeVersions(country: string) { version_list = []; - sendMessage({ + await sendMessage({ type: 'recipe_versions', payload: { auth: '',