mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-07 00:52:45 +02:00
private key management for meshcore
This commit is contained in:
@@ -27,6 +27,20 @@ function formatLocalTime(utcString: string): string {
|
||||
return utcDate.toLocaleString();
|
||||
}
|
||||
|
||||
function ChatMessageItem({ msg }: { msg: ChatMessage }) {
|
||||
// Placeholder for decryption logic
|
||||
// const decryptedMessage = decryptMessage(msg.encrypted_message);
|
||||
return (
|
||||
<div className="border-b border-gray-200 dark:border-neutral-800 pb-2 mb-2">
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(msg.ingest_timestamp)} <span className="text-xs text-blue-600 dark:text-blue-400 font-mono">{msg.channel_hash}</span>
|
||||
</div>
|
||||
<div className="font-mono break-all whitespace-pre-wrap">{formatHex(msg.encrypted_message)}</div>
|
||||
<div className="text-xs text-gray-300">from: {msg.origin}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChatBox() {
|
||||
const [minimized, setMinimized] = useState(true);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
@@ -100,13 +114,7 @@ export default function ChatBox() {
|
||||
<div className="text-gray-400 text-center mt-8">No chat messages found.</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.ingest_timestamp + msg.origin + i} className="border-b border-gray-200 dark:border-neutral-800 pb-2 mb-2">
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(msg.ingest_timestamp)} <span className="text-xs text-blue-600 dark:text-blue-400 font-mono">{msg.channel_hash}</span>
|
||||
</div>
|
||||
<div className="font-mono break-all whitespace-pre-wrap">{formatHex(msg.encrypted_message)}</div>
|
||||
<div className="text-xs text-gray-300">from: {msg.origin}</div>
|
||||
</div>
|
||||
<ChatMessageItem key={msg.ingest_timestamp + msg.origin + i} msg={msg} />
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
"use client";
|
||||
import React, { createContext, useContext, useState, useEffect, useRef, useLayoutEffect, ReactNode } from "react";
|
||||
import { getChannelIdFromKey } from "../lib/meshcore";
|
||||
|
||||
// Config shape
|
||||
type NodeType = "meshcore" | "meshtastic";
|
||||
export type MeshcoreKey = {
|
||||
channelName: string;
|
||||
privateKey: string;
|
||||
};
|
||||
export type Config = {
|
||||
nodeTypes: NodeType[]; // which node types to show
|
||||
lastSeen: number | null; // seconds, or null for forever
|
||||
tileLayer: string; // add tileLayer selection
|
||||
clustering?: boolean; // add clustering toggle
|
||||
showNodeNames?: boolean; // add show node names toggle
|
||||
meshcoreKeys?: MeshcoreKey[]; // meshcore private keys
|
||||
};
|
||||
|
||||
const TILE_LAYERS = [
|
||||
@@ -23,6 +29,7 @@ const DEFAULT_CONFIG: Config = {
|
||||
tileLayer: "openstreetmap", // default
|
||||
clustering: true, // default to clustering enabled
|
||||
showNodeNames: true, // default to show node names
|
||||
meshcoreKeys: [], // default empty
|
||||
};
|
||||
|
||||
const LAST_SEEN_OPTIONS = [
|
||||
@@ -36,11 +43,17 @@ const LAST_SEEN_OPTIONS = [
|
||||
{ value: null, label: "Forever (all time)" },
|
||||
];
|
||||
|
||||
const PUBLIC_MESHCORE_KEY = {
|
||||
channelName: "Public",
|
||||
privateKey: "izOH6cXN6mrJ5e26oRXNcg==",
|
||||
};
|
||||
|
||||
const ConfigContext = createContext<any>(null);
|
||||
|
||||
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [config, setConfig] = useState<Config>(DEFAULT_CONFIG);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [keyModalOpen, setKeyModalOpen] = useState(false);
|
||||
const configButtonRef = useRef<HTMLElement | null>(null);
|
||||
const firstRender = useRef(true);
|
||||
|
||||
@@ -70,7 +83,14 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, setConfig, openConfig, configButtonRef }}>
|
||||
{children}
|
||||
{open && <ConfigPopover config={config} setConfig={setConfig} onClose={closeConfig} anchorRef={configButtonRef} />}
|
||||
{open && <ConfigPopover config={config} setConfig={setConfig} onClose={closeConfig} anchorRef={configButtonRef} onOpenKeyModal={() => setKeyModalOpen(true)} />}
|
||||
{keyModalOpen && (
|
||||
<MeshcoreKeyModal
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
onClose={() => setKeyModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -79,7 +99,7 @@ export function useConfig() {
|
||||
return useContext(ConfigContext);
|
||||
}
|
||||
|
||||
function ConfigPopover({ config, setConfig, onClose, anchorRef }: { config: Config, setConfig: (c: Config) => void, onClose: () => void, anchorRef: React.RefObject<HTMLElement | null> }) {
|
||||
function ConfigPopover({ config, setConfig, onClose, anchorRef, onOpenKeyModal }: { config: Config, setConfig: (c: Config) => void, onClose: () => void, anchorRef: React.RefObject<HTMLElement | null>, onOpenKeyModal: () => void }) {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Click outside to close
|
||||
@@ -194,6 +214,134 @@ function ConfigPopover({ config, setConfig, onClose, anchorRef }: { config: Conf
|
||||
<span>Show node names</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<button
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 w-full"
|
||||
onClick={onOpenKeyModal}
|
||||
>
|
||||
Manage Meshcore Private Keys
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function validateMeshcoreKey(base64Key: string): string | null {
|
||||
if (!base64Key) return null;
|
||||
try {
|
||||
const bytes = Buffer.from(base64Key, 'base64');
|
||||
if (bytes.length !== 16) {
|
||||
return 'Key must decode to exactly 16 bytes.';
|
||||
}
|
||||
} catch {
|
||||
return 'Invalid base64 encoding.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function MeshcoreKeyModal({ config, setConfig, onClose }: { config: Config, setConfig: (c: Config) => void, onClose: () => void }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[3000] flex items-center justify-center bg-black bg-opacity-40">
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-lg p-6 min-w-[350px] max-w-[90vw] max-h-[60vh] overflow-auto border border-gray-200 dark:border-neutral-700 relative">
|
||||
<button
|
||||
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||
onClick={onClose}
|
||||
aria-label="Close key modal"
|
||||
>
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold mb-4">Meshcore Private Keys</h2>
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
These keys will be used to decrypt messages. <b>Your keys are never shared with the server</b>, so your messages remain secure.
|
||||
</p>
|
||||
<div className="flex flex-col gap-1 mb-2 p-2 border rounded bg-gray-100 dark:bg-neutral-700 opacity-80">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 p-1 border rounded bg-gray-200 dark:bg-neutral-800 cursor-not-allowed"
|
||||
type="text"
|
||||
value={PUBLIC_MESHCORE_KEY.channelName}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
<span className="text-xs text-gray-500">ID: <span className="font-mono">{getChannelIdFromKey(PUBLIC_MESHCORE_KEY.privateKey)}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
className="flex-1 p-1 border rounded font-mono bg-gray-200 dark:bg-neutral-800 cursor-not-allowed"
|
||||
type="text"
|
||||
value={PUBLIC_MESHCORE_KEY.privateKey}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
className="text-gray-400 px-2 py-1 cursor-not-allowed"
|
||||
disabled
|
||||
aria-label="Remove key"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{(config.meshcoreKeys || []).map((key, idx) => {
|
||||
const keyError = validateMeshcoreKey(key.privateKey);
|
||||
return (
|
||||
<div key={idx} className="flex flex-col gap-1 mb-2 p-2 border rounded bg-gray-50 dark:bg-neutral-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 p-1 border rounded"
|
||||
type="text"
|
||||
placeholder="Channel Name"
|
||||
value={key.channelName}
|
||||
onChange={e => {
|
||||
const updated = [...(config.meshcoreKeys || [])];
|
||||
updated[idx] = { ...updated[idx], channelName: e.target.value };
|
||||
setConfig({ ...config, meshcoreKeys: updated });
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">ID: <span className="font-mono">{getChannelIdFromKey(key.privateKey)}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
className={`flex-1 p-1 border rounded font-mono ${keyError ? 'border-red-500' : ''}`}
|
||||
type="text"
|
||||
placeholder="Base64 Private Key"
|
||||
value={key.privateKey}
|
||||
onChange={e => {
|
||||
const updated = [...(config.meshcoreKeys || [])];
|
||||
updated[idx] = { ...updated[idx], privateKey: e.target.value };
|
||||
setConfig({ ...config, meshcoreKeys: updated });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 px-2 py-1"
|
||||
onClick={() => {
|
||||
const updated = [...(config.meshcoreKeys || [])];
|
||||
updated.splice(idx, 1);
|
||||
setConfig({ ...config, meshcoreKeys: updated });
|
||||
}}
|
||||
aria-label="Remove key"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
{keyError && (
|
||||
<div className="text-xs text-red-500 mt-1">{keyError}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
className="mt-2 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
setConfig({
|
||||
...config,
|
||||
meshcoreKeys: [...(config.meshcoreKeys || []), { channelName: '', privateKey: '' }],
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Meshcore Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
// Module-level cache for channel IDs
|
||||
const channelIdCache: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Returns the channel id for a given base64-encoded key.
|
||||
* Decodes the key, hashes it with SHA-256, and returns the first byte as hex.
|
||||
* Results are cached for performance.
|
||||
*/
|
||||
export function getChannelIdFromKey(base64Key: string): string {
|
||||
if (channelIdCache[base64Key]) return channelIdCache[base64Key];
|
||||
const keyBytes = Buffer.from(base64Key, 'base64');
|
||||
const hash = createHash('sha256').update(keyBytes).digest();
|
||||
const id = hash[0].toString(16).padStart(2, '0');
|
||||
channelIdCache[base64Key] = id;
|
||||
return id;
|
||||
}
|
||||
Reference in New Issue
Block a user