From 93df18374143925265e3f5066ad87f3f9c869126 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Thu, 18 Dec 2025 20:36:32 -0500 Subject: [PATCH 1/6] Update version badge to 1.2.0 in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8815eb4..3fb03b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeshCore GOME WarDriver -[![Version](https://img.shields.io/badge/version-1.0.1-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.0.1) +[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.2.0) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Android%20%7C%20iOS-orange.svg)](#platform-support) From 4c25e0f08b5994c8e2de069b9740a3566748a088 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 02:08:35 +0000 Subject: [PATCH 2/6] Initial plan From 24583f3172a4816d255ce96b565dafe9966c4aee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 02:15:47 +0000 Subject: [PATCH 3/6] Implement auto-create #wardriving channel with dynamic key derivation Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 110 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 3e23b20..de9416c 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -44,10 +44,6 @@ const STATUS_UPDATE_DELAY_MS = 100; // Brief delay to ensure "Ping se const MAP_REFRESH_DELAY_MS = 1000; // Delay after API post to ensure backend updated const MIN_PAUSE_THRESHOLD_MS = 1000; // Minimum timer value (1 second) to pause const MAX_REASONABLE_TIMER_MS = 5 * 60 * 1000; // Maximum reasonable timer value (5 minutes) to handle clock skew -const WARDROVE_KEY = new Uint8Array([ - 0x40, 0x76, 0xC3, 0x15, 0xC1, 0xEF, 0x38, 0x5F, - 0xA9, 0x3F, 0x06, 0x60, 0x27, 0x32, 0x0F, 0xE5 -]); // Ottawa Geofence Configuration const OTTAWA_CENTER_LAT = 45.4215; // Parliament Hill latitude @@ -714,8 +710,92 @@ async function primeGpsOnce() { } +// ---- Key Derivation ---- +/** + * Derives a 16-byte channel key from a hashtag channel name using SHA-256. + * This allows any hashtag channel to be used (e.g., #wardriving, #wardrive, #test). + * Channel names must start with # and contain only a-z, 0-9, and dashes. + * + * Algorithm: sha256(channelName).subarray(0, 16) + * + * @param {string} channelName - The hashtag channel name (e.g., "#wardriving") + * @returns {Promise} A 16-byte key derived from the channel name + * @throws {Error} If channel name format is invalid + */ +async function deriveChannelKey(channelName) { + // Validate channel name format: must start with # and contain only a-z, 0-9, and dashes + if (!channelName.startsWith('#')) { + throw new Error(`Channel name must start with # (got: "${channelName}")`); + } + + // Check that the part after # contains only lowercase letters, numbers, and dashes + const nameWithoutHash = channelName.slice(1); + if (!/^[a-z0-9-]+$/.test(nameWithoutHash)) { + throw new Error( + `Channel name "${channelName}" contains invalid characters. Only a-z, 0-9, and dashes are allowed.` + ); + } + + // Encode the channel name as UTF-8 + const encoder = new TextEncoder(); + const data = encoder.encode(channelName); + + // Hash using SHA-256 + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + + // Take the first 16 bytes of the hash as the channel key + // This matches the pseudocode: sha256(#name).subarray(0, 16) + const hashArray = new Uint8Array(hashBuffer); + const channelKey = hashArray.slice(0, 16); + + debugLog(`Derived channel key for "${channelName}": ${Array.from(channelKey).map(b => b.toString(16).padStart(2, '0')).join('')}`); + + return channelKey; +} // ---- Channel helpers ---- +async function createWardriveChannel() { + if (!state.connection) throw new Error("Not connected"); + + debugLog(`Attempting to create channel: ${CHANNEL_NAME}`); + + // Get all channels + const channels = await state.connection.getChannels(); + debugLog(`Retrieved ${channels.length} channels`); + + // Find first empty channel slot + let emptyIdx = -1; + for (let i = 0; i < channels.length; i++) { + if (channels[i].name === '') { + emptyIdx = i; + debugLog(`Found empty channel slot at index: ${emptyIdx}`); + break; + } + } + + // Throw error if no free slots + if (emptyIdx === -1) { + debugError(`No empty channel slots available`); + throw new Error( + `No empty channel slots available. Please free a channel slot on your companion first.` + ); + } + + // Derive the channel key from the channel name + const channelKey = await deriveChannelKey(CHANNEL_NAME); + + // Create the channel + debugLog(`Creating channel ${CHANNEL_NAME} at index ${emptyIdx}`); + await state.connection.setChannel(emptyIdx, CHANNEL_NAME, channelKey); + debugLog(`Channel ${CHANNEL_NAME} created successfully at index ${emptyIdx}`); + + // Return channel object + return { + channelIdx: emptyIdx, + name: CHANNEL_NAME + }; +} + async function ensureChannel() { if (!state.connection) throw new Error("Not connected"); if (state.channel) { @@ -724,16 +804,24 @@ async function ensureChannel() { } debugLog(`Looking up channel: ${CHANNEL_NAME}`); - const ch = await state.connection.findChannelByName(CHANNEL_NAME); + let ch = await state.connection.findChannelByName(CHANNEL_NAME); + if (!ch) { - debugError(`Channel ${CHANNEL_NAME} not found on device`); - enableControls(false); - throw new Error( - `Channel ${CHANNEL_NAME} not found. Join it on your companion first.` - ); + debugLog(`Channel ${CHANNEL_NAME} not found, attempting to create it`); + try { + ch = await createWardriveChannel(); + debugLog(`Channel ${CHANNEL_NAME} created successfully`); + } catch (e) { + debugError(`Failed to create channel ${CHANNEL_NAME}: ${e.message}`); + enableControls(false); + throw new Error( + `Channel ${CHANNEL_NAME} not found and could not be created: ${e.message}` + ); + } + } else { + debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); } - debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); state.channel = ch; enableControls(true); channelInfoEl.textContent = `${CHANNEL_NAME} (CH:${ch.channelIdx})`; From 41793944a8a017b5b0afd1397997140ed5ae6067 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 02:17:21 +0000 Subject: [PATCH 4/6] Add Web Crypto API availability check and remove sensitive key logging Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/content/wardrive.js b/content/wardrive.js index de9416c..30c5b94 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -723,6 +723,13 @@ async function primeGpsOnce() { * @throws {Error} If channel name format is invalid */ async function deriveChannelKey(channelName) { + // Check if Web Crypto API is available + if (typeof crypto === 'undefined' || !crypto.subtle) { + throw new Error( + 'Web Crypto API is not available. This app requires HTTPS or a modern browser with crypto.subtle support.' + ); + } + // Validate channel name format: must start with # and contain only a-z, 0-9, and dashes if (!channelName.startsWith('#')) { throw new Error(`Channel name must start with # (got: "${channelName}")`); @@ -748,7 +755,7 @@ async function deriveChannelKey(channelName) { const hashArray = new Uint8Array(hashBuffer); const channelKey = hashArray.slice(0, 16); - debugLog(`Derived channel key for "${channelName}": ${Array.from(channelKey).map(b => b.toString(16).padStart(2, '0')).join('')}`); + debugLog(`Derived channel key for "${channelName}" (length: ${channelKey.length} bytes)`); return channelKey; } From 066cb0af24efa26ec450772b5940be4bf7c68b9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 02:19:11 +0000 Subject: [PATCH 5/6] Add automatic channel cleanup on disconnect Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/content/wardrive.js b/content/wardrive.js index 30c5b94..4169c71 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1414,6 +1414,18 @@ async function disconnect() { connectBtn.disabled = true; setStatus("Disconnecting...", STATUS_COLORS.info); + // Delete the wardriving channel before disconnecting + try { + if (state.channel && typeof state.connection.deleteChannel === "function") { + debugLog(`Deleting channel ${CHANNEL_NAME} at index ${state.channel.channelIdx}`); + await state.connection.deleteChannel(state.channel.channelIdx); + debugLog(`Channel ${CHANNEL_NAME} deleted successfully`); + } + } catch (e) { + debugWarn(`Failed to delete channel ${CHANNEL_NAME}: ${e.message}`); + // Don't fail disconnect if channel deletion fails + } + try { // WebBleConnection typically exposes one of these. if (typeof state.connection.close === "function") { From 8ac22e4d0eedb81b811059e3391ee882dd312457 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 02:20:35 +0000 Subject: [PATCH 6/6] Normalize channel names to lowercase and improve debug logging Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 4169c71..bc9596e 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -730,22 +730,25 @@ async function deriveChannelKey(channelName) { ); } - // Validate channel name format: must start with # and contain only a-z, 0-9, and dashes + // Validate channel name format: must start with # and contain only letters, numbers, and dashes if (!channelName.startsWith('#')) { throw new Error(`Channel name must start with # (got: "${channelName}")`); } - // Check that the part after # contains only lowercase letters, numbers, and dashes - const nameWithoutHash = channelName.slice(1); + // Normalize channel name to lowercase (MeshCore convention) + const normalizedName = channelName.toLowerCase(); + + // Check that the part after # contains only letters, numbers, and dashes + const nameWithoutHash = normalizedName.slice(1); if (!/^[a-z0-9-]+$/.test(nameWithoutHash)) { throw new Error( - `Channel name "${channelName}" contains invalid characters. Only a-z, 0-9, and dashes are allowed.` + `Channel name "${channelName}" contains invalid characters. Only letters, numbers, and dashes are allowed.` ); } - // Encode the channel name as UTF-8 + // Encode the normalized channel name as UTF-8 const encoder = new TextEncoder(); - const data = encoder.encode(channelName); + const data = encoder.encode(normalizedName); // Hash using SHA-256 const hashBuffer = await crypto.subtle.digest('SHA-256', data); @@ -755,7 +758,7 @@ async function deriveChannelKey(channelName) { const hashArray = new Uint8Array(hashBuffer); const channelKey = hashArray.slice(0, 16); - debugLog(`Derived channel key for "${channelName}" (length: ${channelKey.length} bytes)`); + debugLog(`Channel key derived successfully (${channelKey.length} bytes)`); return channelKey; }