Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Security tab #275

Merged
merged 14 commits into from
Aug 21, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.8.2",
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240613143006-244927bc441a.1",
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1",
"@types/chrome": "^0.0.263",
"@types/node": "^20.14.9",
"@types/react": "^18.3.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/components/Form/DynamicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "react-hook-form";

interface DisabledBy<T> {
fieldName: Path<T>;
fieldName: Path<T> | "always";
selector?: number;
invert?: boolean;
}
Expand Down Expand Up @@ -66,7 +66,9 @@ export function DynamicForm<T extends FieldValues>({
if (!disabledBy) return false;

return disabledBy.some((field) => {
if (field.fieldName === "always") return true;
const value = getValues(field.fieldName);
if (value === "always") return true;
if (typeof value === "boolean") return field.invert ? value : !value;
if (typeof value === "number")
return field.invert
Expand Down
19 changes: 0 additions & 19 deletions src/components/PageComponents/Config/Device.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,6 @@ export const Device = (): JSX.Element => {
formatEnumName: true,
},
},
{
type: "toggle",
name: "serialEnabled",
label: "Serial Output Enabled",
description: "Enable the device's serial console",
},
{
type: "toggle",
name: "debugLogEnabled",
label: "Enabled Debug Log",
description:
"Output debugging information to the device's serial port (auto disables when serial client is connected)",
},
{
type: "number",
name: "buttonGpio",
Expand Down Expand Up @@ -86,12 +73,6 @@ export const Device = (): JSX.Element => {
label: "Double Tap as Button Press",
description: "Treat double tap as button press",
},
{
type: "toggle",
name: "isManaged",
label: "Managed",
description: "Is this device managed by a mesh administator",
},
{
type: "toggle",
name: "disableTripleClick",
Expand Down
142 changes: 142 additions & 0 deletions src/components/PageComponents/Config/Security.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { DynamicForm } from "@app/components/Form/DynamicForm.js";
import type { SecurityValidation } from "@app/validation/config/security.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js";
import { Eye, EyeOff } from "lucide-react";
import { useState } from "react";

export const Security = (): JSX.Element => {
const { config, nodes, hardware, setWorkingConfig } = useDevice();

const [privateKey, setPrivateKey] = useState<string>(
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
);
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
const [publicKey, setPublicKey] = useState<string>(
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
);
const [adminKey, setAdminKey] = useState<string>(
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
);
const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false);

const onSubmit = (data: SecurityValidation) => {
setWorkingConfig(
new Protobuf.Config.Config({
payloadVariant: {
case: "security",
value: {
...data,
adminKey: toByteArray(adminKey),
privateKey: toByteArray(privateKey),
publicKey: toByteArray(publicKey),
},
},
}),
);
};
return (
<DynamicForm<SecurityValidation>
onSubmit={onSubmit}
defaultValues={{
...config.security,
adminKey: adminKey,
privateKey: privateKey,
publicKey: publicKey,
}}
fieldGroups={[
{
label: "Security Settings",
description: "Settings for the Security configuration",
fields: [
{
type: privateKeyVisible ? "text" : "password",
name: "privateKey",
label: "Private Key",
description: "Used to create a shared key with a remote device",
disabledBy: [
{
fieldName: "adminChannelEnabled",
invert: true,
},
],
properties: {
action: {
icon: privateKeyVisible ? EyeOff : Eye,
onClick: () => setPrivateKeyVisible(!privateKeyVisible),
},
},
},
{
type: "text",
name: "publicKey",
label: "Public Key",
description:
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
disabledBy: [{ fieldName: "always" }],
},
],
},
{
label: "Admin Settings",
description: "Settings for Admin ",
fields: [
{
type: "toggle",
name: "adminChannelEnabled",
label: "Allow Legacy Admin",
description:
"Allow incoming device control over the insecure legacy admin channel",
},
{
type: "toggle",
name: "isManaged",
label: "Managed",
description:
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
},
{
type: adminKeyVisible ? "text" : "password",
name: "adminKey",
label: "Admin Key",
disabledBy: [{ fieldName: "adminChannelEnabled" }],
properties: {
action: {
icon: adminKeyVisible ? EyeOff : Eye,
onClick: () => setAdminKeyVisible(!adminKeyVisible),
},
},
description:
"The public key authorized to send admin messages to this node",
},
],
},
{
label: "Logging Settings",
description: "Settings for Logging",
fields: [
{
type: "toggle",
name: "bluetoothLoggingEnabled",
label: "Allow Bluetooth Logging",
description: "Enables device (serial style logs) over Bluetooth",
},
{
type: "toggle",
name: "debugLogApiEnabled",
label: "Enable Debug Log API",
description: "Output live debug logging over serial",
},
{
type: "toggle",
name: "serialEnabled",
label: "Serial Output Enabled",
description: "Serial Console over the Stream API",
},
],
},
]}
/>
);
};
2 changes: 1 addition & 1 deletion src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
<div className="flex items-center">
<ZapIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>
{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
</Subtle>
</div>
<div className="flex items-center">
Expand Down
3 changes: 3 additions & 0 deletions src/core/stores/deviceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
device.config.bluetooth = config.payloadVariant.value;
break;
}
case "security": {
device.config.security = config.payloadVariant.value;
}
}
}
}),
Expand Down
5 changes: 5 additions & 0 deletions src/pages/Config/DeviceConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LoRa } from "@components/PageComponents/Config/LoRa.js";
import { Network } from "@components/PageComponents/Config/Network.js";
import { Position } from "@components/PageComponents/Config/Position.js";
import { Power } from "@components/PageComponents/Config/Power.js";
import { Security } from "@components/PageComponents/Config/Security.js";
import {
Tabs,
TabsContent,
Expand Down Expand Up @@ -47,6 +48,10 @@ export const DeviceConfig = (): JSX.Element => {
label: "Bluetooth",
element: Bluetooth,
},
{
label: "Security",
element: Security,
},
];

return (
Expand Down
8 changes: 4 additions & 4 deletions src/pages/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ export const MapPage = (): JSX.Element => {
{waypoints.map((wp) => (
<Marker
key={wp.id}
longitude={wp.longitudeI / 1e7}
latitude={wp.latitudeI / 1e7}
longitude={(wp.longitudeI ?? 0) / 1e7}
latitude={(wp.latitudeI ?? 0) / 1e7}
anchor="bottom"
>
<div>
Expand All @@ -163,8 +163,8 @@ export const MapPage = (): JSX.Element => {
return (
<Marker
key={node.num}
longitude={node.position.longitudeI / 1e7}
latitude={node.position.latitudeI / 1e7}
longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7}
style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
Expand Down
6 changes: 5 additions & 1 deletion src/validation/config/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Protobuf } from "@meshtastic/js";
import { IsBoolean, IsEnum, IsInt } from "class-validator";

export class BluetoothValidation
implements Omit<Protobuf.Config.Config_BluetoothConfig, keyof Message>
implements
Omit<
Protobuf.Config.Config_BluetoothConfig,
keyof Message | "deviceLoggingEnabled"
>
{
@IsBoolean()
enabled: boolean;
Expand Down
3 changes: 2 additions & 1 deletion src/validation/config/lora.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Protobuf } from "@meshtastic/js";
import { IsArray, IsBoolean, IsEnum, IsInt, Max, Min } from "class-validator";

export class LoRaValidation
implements Omit<Protobuf.Config.Config_LoRaConfig, keyof Message>
implements
Omit<Protobuf.Config.Config_LoRaConfig, keyof Message | "paFanDisabled">
{
@IsBoolean()
usePreset: boolean;
Expand Down
3 changes: 2 additions & 1 deletion src/validation/config/power.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { Protobuf } from "@meshtastic/js";
import { IsBoolean, IsInt, IsNumber, Max, Min } from "class-validator";

export class PowerValidation
implements Omit<Protobuf.Config.Config_PowerConfig, keyof Message>
implements
Omit<Protobuf.Config.Config_PowerConfig, keyof Message | "powermonEnables">
{
@IsBoolean()
isPowerSaving: boolean;
Expand Down
35 changes: 35 additions & 0 deletions src/validation/config/security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Message } from "@bufbuild/protobuf";
import type { Protobuf } from "@meshtastic/js";
import { IsBoolean, IsString } from "class-validator";

export class SecurityValidation
implements
Omit<
Protobuf.Config.Config_SecurityConfig,
keyof Message | "adminKey" | "privateKey" | "publicKey"
>
{
@IsBoolean()
adminChannelEnabled: boolean;

@IsString()
adminKey: string;

@IsBoolean()
bluetoothLoggingEnabled: boolean;

@IsBoolean()
debugLogApiEnabled: boolean;

@IsBoolean()
isManaged: boolean;

@IsString()
privateKey: string;

@IsString()
publicKey: string;

@IsBoolean()
serialEnabled: boolean;
}
5 changes: 4 additions & 1 deletion src/validation/moduleConfig/storeForward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { IsBoolean, IsInt } from "class-validator";

export class StoreForwardValidation
implements
Omit<Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig, keyof Message>
Omit<
Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig,
keyof Message | "isServer"
>
{
@IsBoolean()
enabled: boolean;
Expand Down