From d8d21ad58ab5d6112516e324a805aae1f3902b55 Mon Sep 17 00:00:00 2001 From: philon- Date: Wed, 14 May 2025 21:38:08 +0200 Subject: [PATCH] Refactoring Security Config --- src/components/Form/FormToggle.tsx | 17 +- .../Config/Security/Security.tsx | 149 ++++++++++++++---- .../Config/Security/securityReducer.tsx | 4 +- .../PageComponents/Config/Security/types.ts | 16 +- 4 files changed, 141 insertions(+), 45 deletions(-) diff --git a/src/components/Form/FormToggle.tsx b/src/components/Form/FormToggle.tsx index d1b0567b..4b006c2f 100644 --- a/src/components/Form/FormToggle.tsx +++ b/src/components/Form/FormToggle.tsx @@ -3,11 +3,11 @@ import type { GenericFormElementProps, } from "@components/Form/DynamicForm.tsx"; import { Switch } from "@components/UI/Switch.tsx"; -import type { ChangeEvent } from "react"; import { Controller, type FieldValues } from "react-hook-form"; export interface ToggleFieldProps extends BaseFormBuilderProps { type: "toggle"; + inputChange?: (value: boolean) => void; } export function ToggleInput({ @@ -15,16 +15,6 @@ export function ToggleInput({ disabled, field, }: GenericFormElementProps>) { - const onChangeHandler = (e: (event: ChangeEvent) => void) => { - return (value: boolean) => { - e({ - target: { - value: value, - }, - } as unknown as ChangeEvent); - }; - }; - return ( ({ render={({ field: { value, onChange, ...rest } }) => ( { + onChange(v); + field.inputChange?.(v); + }} id={field.name} disabled={disabled} {...field.properties} diff --git a/src/components/PageComponents/Config/Security/Security.tsx b/src/components/PageComponents/Config/Security/Security.tsx index 496d4964..e16b983e 100644 --- a/src/components/PageComponents/Config/Security/Security.tsx +++ b/src/components/PageComponents/Config/Security/Security.tsx @@ -9,6 +9,7 @@ import { Protobuf } from "@meshtastic/core"; import { fromByteArray, toByteArray } from "base64-js"; import { useReducer } from "react"; import { securityReducer } from "@components/PageComponents/Config/Security/securityReducer.tsx"; +import type { SecurityConfigInit } from "./types.ts"; export const Security = () => { const { config, setWorkingConfig, setDialogOpen } = useDevice(); @@ -33,6 +34,10 @@ export const Security = () => { fromByteArray(config.security?.adminKey?.at(2) ?? new Uint8Array(0)), ], privateKeyDialogOpen: false, + isManaged: config.security?.isManaged ?? false, + adminChannelEnabled: config.security?.adminChannelEnabled ?? false, + debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false, + serialEnabled: config.security?.serialEnabled ?? false, }); const validateKey = ( @@ -44,7 +49,6 @@ export const Security = () => { const fieldNameKey = fieldName + (fieldIndex ?? ""); try { removeError(fieldNameKey); - if (fieldName === "privateKey" && input === "") { addError(fieldNameKey, "Private Key is required"); return; @@ -80,29 +84,24 @@ export const Security = () => { } }; - const onSubmit = (data: SecurityValidation) => { - if (hasErrors()) { - return; - } - - setWorkingConfig( - create(Protobuf.Config.ConfigSchema, { - payloadVariant: { - case: "security", - value: { - ...data, - adminKey: [ - toByteArray(state.adminKey[0]), - toByteArray(state.adminKey[1]), - toByteArray(state.adminKey[2]), - ], - privateKey: toByteArray(state.privateKey), - publicKey: toByteArray(state.publicKey), - }, - }, - }), - ); - }; + function buildSecurityPayload( + overrides: SecurityConfigInit, + ) { + const base: SecurityConfigInit = { + isManaged: state.isManaged, + adminChannelEnabled: state.adminChannelEnabled, + debugLogApiEnabled: state.debugLogApiEnabled, + serialEnabled: state.serialEnabled, + privateKey: overrides?.privateKey ?? toByteArray(state.privateKey), + publicKey: overrides?.publicKey ?? toByteArray(state.publicKey), + adminKey: [ + overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]), + overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]), + overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]), + ], + }; + return { ...base, ...overrides }; + } const pkiRegenerate = () => { clearErrors(); @@ -122,6 +121,20 @@ export const Security = () => { state.privateKeyBitCount, "privateKey", ); + + if (!hasErrors()) { + setWorkingConfig( + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "security", + value: buildSecurityPayload({ + privateKey: privateKey, + publicKey: publicKey, + }), + }, + }), + ); + } }; const privateKeyInputChangeEvent = ( @@ -133,6 +146,20 @@ export const Security = () => { const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String)); dispatch({ type: "SET_PUBLIC_KEY", payload: fromByteArray(publicKey) }); + + if (!hasErrors()) { + setWorkingConfig( + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "security", + value: buildSecurityPayload({ + privateKey: toByteArray(privateKeyB64String), + publicKey: publicKey, + }), + }, + }), + ); + } }; const adminKeyInputChangeEvent = ( @@ -150,18 +177,62 @@ export const Security = () => { dispatch({ type: "SET_ADMIN_KEY", payload: payload }); validateKey(psk, state.privateKeyBitCount, "adminKey", fieldIndex); + + if (!hasErrors()) { + setWorkingConfig( + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "security", + value: buildSecurityPayload({ + adminKey: payload.map(toByteArray) as [ + Uint8Array, + Uint8Array, + Uint8Array, + ], + }), + }, + }), + ); + } }; - const privateKeySelectChangeEvent = (e: string) => { - const count = Number.parseInt(e); - dispatch({ type: "SET_PRIVATE_KEY_BIT_COUNT", payload: count }); - validateKey(state.privateKey, count, "privateKey"); + const onToggleChange = ( + field: + | "isManaged" + | "adminChannelEnabled" + | "debugLogApiEnabled" + | "serialEnabled", + next: boolean, + ) => { + dispatch({ type: "SET_TOGGLE", field, payload: next }); + + if (!hasErrors()) { + setWorkingConfig( + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "security", + value: buildSecurityPayload({ + isManaged: field === "isManaged" ? next : state.isManaged, + adminChannelEnabled: field === "adminChannelEnabled" + ? next + : state.adminChannelEnabled, + debugLogApiEnabled: field === "debugLogApiEnabled" + ? next + : state.debugLogApiEnabled, + serialEnabled: field === "serialEnabled" + ? next + : state.serialEnabled, + }), + }, + }), + ); + } }; return ( <> - onSubmit={onSubmit} + onSubmit={() => {}} submitType="onChange" defaultValues={{ ...config.security, @@ -192,7 +263,7 @@ export const Security = () => { : "", devicePSKBitCount: state.privateKeyBitCount, inputChange: privateKeyInputChangeEvent, - selectChange: privateKeySelectChangeEvent, + selectChange: () => {}, hide: !state.privateKeyVisible, actionButtons: [ { @@ -315,6 +386,10 @@ export const Security = () => { label: "Managed", description: "If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below.", + inputChange: (e: boolean) => onToggleChange("isManaged", e), + properties: { + checked: state.isManaged, + }, }, { type: "toggle", @@ -322,6 +397,11 @@ export const Security = () => { label: "Allow Legacy Admin", description: "Allow incoming device control over the insecure legacy admin channel", + inputChange: (e: boolean) => + onToggleChange("adminChannelEnabled", e), + properties: { + checked: state.adminChannelEnabled, + }, }, ], }, @@ -335,12 +415,21 @@ export const Security = () => { label: "Enable Debug Log API", description: "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + inputChange: (e: boolean) => + onToggleChange("debugLogApiEnabled", e), + properties: { + checked: state.debugLogApiEnabled, + }, }, { type: "toggle", name: "serialEnabled", label: "Serial Output Enabled", description: "Serial Console over the Stream API", + inputChange: (e: boolean) => onToggleChange("serialEnabled", e), + properties: { + checked: state.serialEnabled, + }, }, ], }, diff --git a/src/components/PageComponents/Config/Security/securityReducer.tsx b/src/components/PageComponents/Config/Security/securityReducer.tsx index a719ff34..4860ab4a 100644 --- a/src/components/PageComponents/Config/Security/securityReducer.tsx +++ b/src/components/PageComponents/Config/Security/securityReducer.tsx @@ -7,8 +7,6 @@ export function securityReducer( switch (action.type) { case "SET_PRIVATE_KEY": return { ...state, privateKey: action.payload }; - case "TOGGLE_PRIVATE_KEY_VISIBILITY": - return { ...state, privateKeyVisible: !state.privateKeyVisible }; case "SET_PRIVATE_KEY_BIT_COUNT": return { ...state, privateKeyBitCount: action.payload }; case "SET_PUBLIC_KEY": @@ -24,6 +22,8 @@ export function securityReducer( publicKey: action.payload.publicKey, privateKeyDialogOpen: false, }; + case "SET_TOGGLE": + return { ...state, [action.field]: action.payload }; default: return state; } diff --git a/src/components/PageComponents/Config/Security/types.ts b/src/components/PageComponents/Config/Security/types.ts index 0f885c31..acfde8ce 100644 --- a/src/components/PageComponents/Config/Security/types.ts +++ b/src/components/PageComponents/Config/Security/types.ts @@ -1,3 +1,6 @@ +import { type MessageInitShape } from "@bufbuild/protobuf"; +import { Protobuf } from "@meshtastic/core"; + export interface SecurityState { privateKey: string; privateKeyVisible: boolean; @@ -6,16 +9,27 @@ export interface SecurityState { publicKey: string; adminKey: [string, string, string]; privateKeyDialogOpen: boolean; + isManaged: boolean; + adminChannelEnabled: boolean; + debugLogApiEnabled: boolean; + serialEnabled: boolean; } export type SecurityAction = | { type: "SET_PRIVATE_KEY"; payload: string } - | { type: "TOGGLE_PRIVATE_KEY_VISIBILITY" } | { type: "SET_PRIVATE_KEY_BIT_COUNT"; payload: number } | { type: "SET_PUBLIC_KEY"; payload: string } | { type: "SET_ADMIN_KEY"; payload: [string, string, string] } | { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean } + | { type: "SET_TOGGLE"; payload: boolean; field: string } | { type: "REGENERATE_PRIV_PUB_KEY"; payload: { privateKey: string; publicKey: string }; }; + +export type SecurityConfigInit = Extract< + MessageInitShape< + typeof Protobuf.Config.ConfigSchema + >["payloadVariant"], + { case: "security" } +>["value"];