From e1f63a82154d3cedac19631c8b2d909d7751d236 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 13 Jun 2026 20:13:12 -0500 Subject: [PATCH] feat: add encryption key bridge for nodejs-mobile local mode - Add src/crypto.ts: generates 48-byte hex key, stores via capacitor-secure-storage-plugin (Android Keystore / iOS Keychain) - Add nodejs-assets/nodejs-project/main.js: waits for encryption key from WebView before starting server - Update LoadingScreen.tsx: sends encryption key to embedded Node process over nodejs-mobile-cordova channel - Add capacitor-secure-storage-plugin dependency - Update vite-env.d.ts with channel types for nodejs - Add private docs to .gitignore (PROJECT.md, PLAN.md, etc.) --- .gitignore | 10 +++++++++- nodejs-assets/nodejs-project/main.js | 24 ++++++++++++++++++++++++ package.json | 1 + src/LoadingScreen.tsx | 15 +++++++++++++++ src/crypto.ts | 28 ++++++++++++++++++++++++++++ src/vite-env.d.ts | 4 ++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 nodejs-assets/nodejs-project/main.js create mode 100644 src/crypto.ts diff --git a/.gitignore b/.gitignore index 0e973b3..6e296ff 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,12 @@ Thumbs.db # IDE .vscode/ -.idea/ \ No newline at end of file +.idea/ + +# Private project/agent docs — never commit +PROJECT.md +PLAN.md +FUTURE.md +HISTORY.md +DEVELOPMENT_LOG.md +.learnings/ \ No newline at end of file diff --git a/nodejs-assets/nodejs-project/main.js b/nodejs-assets/nodejs-project/main.js new file mode 100644 index 0000000..2e989c2 --- /dev/null +++ b/nodejs-assets/nodejs-project/main.js @@ -0,0 +1,24 @@ +'use strict'; + +const path = require('path'); + +// Server-side paths, relative to this directory (the app's writable storage +// on-device — nodejs-mobile-cordova copies nodejs-project here at runtime). +process.env.DB_PATH = path.join(__dirname, 'data', 'bills.db'); +process.env.BACKUP_PATH = path.join(__dirname, 'backups'); +process.env.PORT = process.env.PORT || '3000'; +process.env.BIND_HOST = '127.0.0.1'; + +const cordova = require('cordova-bridge'); + +// Wait for the WebView to hand over the device-bound encryption key (stored +// in Android Keystore / iOS Keychain — see src/crypto.ts) before starting the +// server, so encryptionService.js picks up TOKEN_ENCRYPTION_KEY on first use. +cordova.channel.on('message', function (msg) { + if (msg && msg.type === 'encryptionKey' && typeof msg.key === 'string') { + process.env.TOKEN_ENCRYPTION_KEY = msg.key; + require('./server/server.js'); + } +}); + +cordova.channel.post('message', { type: 'ready' }); diff --git a/package.json b/package.json index b2757fb..3b0b07f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@capacitor/core": "^8.4.0", "@capacitor/ios": "^8.4.0", "@capacitor/preferences": "^8.0.1", + "capacitor-secure-storage-plugin": "^0.13.0", "nodejs-mobile-cordova": "^0.4.3", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/src/LoadingScreen.tsx b/src/LoadingScreen.tsx index 46e1978..90cae10 100644 --- a/src/LoadingScreen.tsx +++ b/src/LoadingScreen.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { getOrCreateEncryptionKey } from './crypto'; const LOCAL_URL = 'http://localhost:3000'; const HEALTH_URL = `${LOCAL_URL}/api/health`; @@ -44,6 +45,20 @@ export default function LoadingScreen({ onReady }: Props) { setError('Failed to start local server: ' + String(err)); return; } + if (cancelled) return; + + // Hand the device-bound encryption key to the embedded Node process over + // the nodejs-mobile-cordova channel. main.js waits for this message + // before requiring server.js, so encryption is configured from the + // first request. The listener is registered synchronously to avoid + // missing main.js's 'ready' message; the key itself is fetched async. + window.nodejs.channel.on('message', msg => { + if (cancelled || (msg as { type?: string })?.type !== 'ready') return; + getOrCreateEncryptionKey().then(key => { + if (!cancelled) window.nodejs!.channel.post('message', { type: 'encryptionKey', key }); + }); + }); + pollHealth(); }); diff --git a/src/crypto.ts b/src/crypto.ts new file mode 100644 index 0000000..4f6d8aa --- /dev/null +++ b/src/crypto.ts @@ -0,0 +1,28 @@ +import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin'; + +const KEY_NAME = 'tokenEncryptionKey'; +const KEY_BYTES = 48; + +function generateHexKey(bytes: number): string { + const arr = new Uint8Array(bytes); + globalThis.crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Returns the device's local-mode database encryption key (TOKEN_ENCRYPTION_KEY), + * generating and persisting one in secure storage (Android Keystore / iOS Keychain) + * on first launch. + */ +export async function getOrCreateEncryptionKey(): Promise { + try { + const { value } = await SecureStoragePlugin.get({ key: KEY_NAME }); + if (value) return value; + } catch { + // Not found — fall through to generate one. + } + + const key = generateHexKey(KEY_BYTES); + await SecureStoragePlugin.set({ key: KEY_NAME, value: key }); + return key; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 8bb0e24..163d207 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,5 +4,9 @@ interface Window { nodejs?: { start: (filename: string, callback?: (err: unknown) => void) => void; + channel: { + on: (event: string, callback: (msg: unknown) => void) => void; + post: (event: string, message: unknown) => void; + }; }; }