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) diff --git a/content/wardrive.js b/content/wardrive.js index 3e23b20..bc9596e 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,102 @@ 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) { + // 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 letters, numbers, and dashes + if (!channelName.startsWith('#')) { + throw new Error(`Channel name must start with # (got: "${channelName}")`); + } + + // 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 letters, numbers, and dashes are allowed.` + ); + } + + // Encode the normalized channel name as UTF-8 + const encoder = new TextEncoder(); + const data = encoder.encode(normalizedName); + + // 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(`Channel key derived successfully (${channelKey.length} bytes)`); + + 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 +814,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})`; @@ -1319,6 +1417,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") {