feature: add secured message

- encrypt/decrypt every message (require ^0.0.2)

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-06-16 10:34:29 +07:00
parent 4ca8b3b270
commit 2a0841a798
14 changed files with 314 additions and 147 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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",

View file

@ -183,9 +183,9 @@
}
}
function saveSheetPrice() {
async function saveSheetPrice() {
if (!canEditSheetPrice || sheetPriceValue === null) return;
sendCommandRequest('sheet', {
await sendCommandRequest('sheet', {
country: get(departmentStore),
content: [
{

View file

@ -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',

View file

@ -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,

View file

@ -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 ?? '',

View file

@ -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<string[]>([]);
type HandshakeAck = { server_public_key: string; status: string };
type WSMessage = { type: string; payload: any };
// MAXIMUM LIMIT = 1814355
@ -131,7 +139,7 @@ const handlers: Record<string, (payload: any) => 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<string, (payload: any) => void> = {
}
// send next chain message
sendMessage({
await sendMessage({
type: 'price',
payload: {
action: {
@ -352,7 +360,7 @@ const handlers: Record<string, (payload: any) => 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<string, (payload: any) => 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<string, (payload: any) => 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<string, (payload: any) => 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);
}

View file

@ -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<string[]>([]);
@ -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<boolean> {
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<boolean> {
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;
}

View file

@ -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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
// 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<boolean> {
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<boolean> {
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'

View file

@ -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<number>(0);
export const socketAlreadySendHeartbeat = writable<number>(0);
export const socketStore = writable<WebSocket | null>(null);
export const sharedKey = writable<CryptoKey | null>(null);
export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
const currentSocket = get(socketStore);
if (currentSocket?.readyState === WebSocket.OPEN) {
@ -49,7 +53,7 @@ export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
});
}
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) {

View file

@ -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: {

View file

@ -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 };
}
}

View file

@ -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);
});
}

View file

@ -58,9 +58,9 @@
}
});
function sendGetRecipeVersions(country: string) {
async function sendGetRecipeVersions(country: string) {
version_list = [];
sendMessage({
await sendMessage({
type: 'recipe_versions',
payload: {
auth: '',