diff --git a/src/components/ConfigContext.tsx b/src/components/ConfigContext.tsx
index 1a8d75d..45a31a1 100644
--- a/src/components/ConfigContext.tsx
+++ b/src/components/ConfigContext.tsx
@@ -1,6 +1,6 @@
"use client";
import React, { createContext, useContext, useState, useEffect, useRef, useLayoutEffect, ReactNode } from "react";
-import { getChannelIdFromKey } from "@/lib/meshcore";
+import { getChannelIdFromKey, deriveKeyFromChannelName } from "@/lib/meshcore";
import { getRegionFriendlyNames } from "@/lib/regions";
import Modal from "./Modal";
@@ -353,7 +353,19 @@ function MeshcoreKeyModal({ config, setConfig, onClose }: { config: Config, setC
value={key.channelName}
onChange={e => {
const updated = [...(config.meshcoreKeys || [])];
- updated[idx] = { ...updated[idx], channelName: e.target.value };
+ const newChannelName = e.target.value;
+ const updatedKey = { ...updated[idx], channelName: newChannelName };
+
+ // Auto-populate private key for # channels
+ if (newChannelName.startsWith('#') && newChannelName.length > 1) {
+ try {
+ updatedKey.privateKey = deriveKeyFromChannelName(newChannelName);
+ } catch (error) {
+ // If derivation fails, leave the key as is
+ }
+ }
+
+ updated[idx] = updatedKey;
setConfig({ ...config, meshcoreKeys: updated });
}}
/>
@@ -373,7 +385,7 @@ function MeshcoreKeyModal({ config, setConfig, onClose }: { config: Config, setC
{
const updated = [...(config.meshcoreKeys || [])];
diff --git a/src/lib/meshcore.ts b/src/lib/meshcore.ts
index cb2fee1..fa20fe3 100644
--- a/src/lib/meshcore.ts
+++ b/src/lib/meshcore.ts
@@ -1,6 +1,31 @@
import { createHash, createHmac } from "crypto";
import aesjs from "aes-js";
+/**
+ * Derives a 128-bit encryption key from a channel name that starts with '#'.
+ * Applies filtering: converts to lowercase and keeps only a-z, 0-9, and hyphen.
+ * Uses SHA256 hash of the filtered channel name (including '#') as ASCII bytes,
+ * then truncates to first 128 bits (16 bytes) and returns as hex.
+ */
+export function deriveKeyFromChannelName(channelName: string): string {
+ if (!channelName.startsWith('#')) {
+ throw new Error('Channel name must start with #');
+ }
+
+ // Apply filtering: lowercase and keep only a-z, 0-9, hyphen
+ const filteredName = '#' + channelName.slice(1)
+ .toLowerCase()
+ .replace(/[^a-z0-9-]/g, '');
+
+ // Convert filtered channel name to ASCII bytes and hash with SHA256
+ const nameBytes = Buffer.from(filteredName, 'ascii');
+ const hash = createHash('sha256').update(nameBytes).digest();
+
+ // Truncate to first 128 bits (16 bytes) and return as hex
+ const key128bit = hash.slice(0, 16);
+ return key128bit.toString('hex');
+}
+
// Module-level cache for channel IDs
const channelIdCache: Record = {};