From 2a0841a798a9ee4059c172a0738ae93f444ecbee Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Tue, 16 Jun 2026 10:34:29 +0700 Subject: [PATCH] feature: add secured message - encrypt/decrypt every message (require ^0.0.2) Signed-off-by: pakintada@gmail.com --- bun.lockb | Bin 197439 -> 198147 bytes package.json | 2 + .../recipe-details/recipe-detail.svelte | 4 +- .../components/recipe-details/value_event.ts | 4 +- .../components/recipe-editor-dialog.svelte | 4 +- src/lib/core/client/server.ts | 4 +- src/lib/core/handlers/messageHandler.ts | 166 +++++++++++------- src/lib/core/handlers/ws_messageSender.ts | 28 ++- src/lib/core/services/sheetService.ts | 94 ++++++---- src/lib/core/stores/websocketStore.ts | 75 +++++--- src/lib/core/types/outMessage.ts | 4 +- src/lib/core/utils/crypto.ts | 68 +++++++ src/routes/(authed)/+layout.svelte | 4 +- .../(authed)/recipe/overview/+page.svelte | 4 +- 14 files changed, 314 insertions(+), 147 deletions(-) create mode 100644 src/lib/core/utils/crypto.ts diff --git a/bun.lockb b/bun.lockb index 9851d42d89cdb58214e184f379e021b0bcb0471e..ce79e9c42e9864bf4221857039a6f68caab2fcf4 100755 GIT binary patch delta 35049 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 delta 34955 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 TP2`eo`l)f7KKMrM6!w1rFcScI 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: '',